MinkowskiEngine 설치

경험 상 MinkowskiEngine을 설치하는 것이 꽤 까다로운 편이다. 그래서 Docker를 사용하는 것을 추천하고, 원작자가 제공한 Docker는 CUDA와 Torch가 너무 옛날버전이어서, 여기 Install MinkowskiEngine 브런치 글을 참고하면 쉽게 docker로 설치할 수 있다.

MinkowskiEngine의 docker 폴더 내의 Dockerfile을 아래와 변경했다 (24년 5월 기준 내 컴퓨터는 Ada Lovelace 기반 GPU를 사용하고 있어서, 11.8이 minimum으로 지원해주는 CUDA 버전이었다. 여기GPU supported 참고”):

# Use use previous versions, modify these variables
# ARG PYTORCH="1.9.0"
# ARG CUDA="11.1"

ARG PYTORCH="2.2.1"
ARG CUDA="11.8"
ARG CUDNN="8"

FROM pytorch/pytorch:${PYTORCH}-cuda${CUDA}-cudnn${CUDNN}-devel

##############################################
# You should modify this to match your GPU compute capability
# ENV TORCH_CUDA_ARCH_LIST="6.0 6.1 7.0+PTX"
ENV TORCH_CUDA_ARCH_LIST="8.6 8.9"
##############################################

ENV TORCH_NVCC_FLAGS="-Xfatbin -compress-all"

# Install dependencies
RUN apt-get update
RUN apt-get install -y git ninja-build cmake build-essential libopenblas-dev \
    xterm xauth openssh-server tmux wget mate-desktop-environment-core

RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

# For faster build, use more jobs.
ENV MAX_JOBS=36
RUN git clone --recursive "https://github.com/leadbreak/MinkowskiEngine"
RUN cd MinkowskiEngine; python setup.py install --force_cuda --blas=openblas

수정후 아래 명령어를 따지면 minkowski_engine이라는 명으로 Docker image를 빌드할 수 있다:

docker build -t minkowski_engine docker

MinkowskiEngine Operation에 대한 배경지식

기억해야할 것은, 이 SparseTensor는 Pytorch의 Tensor와는 다르게, featurescoordinates로 구성되어 있는 점이다. 왜냐하면 2D image와는 다르게 point cloud는 sparse(군데군데 비어있음)하다보니, 이를 잘 표현하기 위해 데이터를 coordinates를 통해 저장하고, 그에 대응하는 features(2D image 상에서 channel이라고 이해하면 쉬울듯)로 나누어서 저장하는 것이다.

이는 사실 그래프 이론에서 graph를 표현할 때 compressed sparse row (CSR) 표현방식 비슷하다 (하지만 원작자의 의도가 아닌 저의 뇌피셜). CSR 형식은 희소 행렬이나 그래프를 저장하는 효율적인 방법인데, 이 형식은 행의 시작 위치와 non-zero 값들, 그리고 해당 값들의 열 인덱스를 별도로 저장한다. CSR은 메모리를 절약하고, 행 기반의 연산을 빠르게 수행할 수 있게 해줘서 논문에 ‘fast graph-theoretic approach’라는 표현이 있으면 거진 이 CSR을 내부적으로 활용하고 있다고 생각하면 편하다.

MinkowskiEngine 더 쉬운 예제 코드

공식 홈페이지 예제코드를 수정해서 어떻게 MinkowskiEngine을 사용하는는지 작성해보았다.

import torch
import MinkowskiEngine as ME

data0 = [
    [0, 0, 2.1, 0, 0],
    [0, 1, 1.4, 3, 0],
    [0, 0, 4.0, 0, 0]
]

data1 = [
    [0, 3.0, 1.9, 0, 0],
    [0, 0.0, 0.0, 0, 0],
    [0, 1.0, 0.0, 0, 7.0]
]

def to_sparse_coo(data):
    # An intuitive way to extract coordinates and features
    coords, feats = [], []
    for i, row in enumerate(data):
        for j, val in enumerate(row):
            if val != 0:
                coords.append([i, j])
                feats.append([val])
    return torch.IntTensor(coords), torch.FloatTensor(feats)

coords0, feats0 = to_sparse_coo(data0)
coords1, feats1 = to_sparse_coo(data1)

voxels0 = torch.FloatTensor([0.5])
voxels1 = torch.FloatTensor([1.5])

coords, feats = ME.utils.sparse_collate([coords0, coords1], [feats0, feats1])
tensor = ME.SparseTensor(feats, coordinates=coords)

print("Original: ")
print(tensor.F.transpose(0, 1))
# => tensor([[2.1000, 1.0000, 1.4000, 3.0000, 4.0000, 3.0000, 1.9000, 1.0000, 7.0000]])

voxels0_expanded = voxels0.expand(coords0.size(0), -1)
voxels1_expanded = voxels1.expand(coords1.size(0), -1)

# We don't need to use `sparse_collate` in this case
# coords, voxels = ME.utils.sparse_collate([coords0, coords1], [voxels0_expanded, voxels1_expanded])
voxels_expanded = torch.cat([voxels0_expanded, voxels1_expanded], dim=0)
tensor1 = ME.SparseTensor(features=voxels_expanded,
                          coordinate_manager=tensor.coordinate_manager,
                          coordinate_map_key=tensor.coordinate_map_key)

print("In place: multiplication")
tensor *= tensor1
print(tensor.F.transpose(0, 1))
# => tensor([[ 1.0500,  0.5000,  0.7000,  1.5000,  2.0000,  4.5000,  2.8500,  1.5000, 10.5000]])

tensor += tensor1
print("In place: addition")
print(tensor.F.transpose(0, 1))
# => tensor([[ 1.5500,  1.0000,  1.2000,  2.0000,  2.5000,  6.0000,  4.3500,  3.0000, 12.0000]])

# Only features whose coordinates are matched with `coords0` are updated
coords_batch0 = tensor.coordinates_at(0)
voxels2_expanded = torch.FloatTensor([100.0]).expand(coords_batch0.size(0), -1)
coords2, voxels2 = ME.utils.sparse_collate([coords_batch0], [voxels2_expanded])
tensor2 = ME.SparseTensor(coordinates=coords2, features=voxels2, coordinate_manager=tensor.coordinate_manager)

tensor = tensor * tensor2

print("After multiplication")
print(tensor.F.transpose(0, 1))
# => tensor([[120.0000, 155.0000, 4.3500, 100.0000, 200.0000, 3.0000, 250.0000, 12.0000, 6.0000]])

# it does not work!
# tensor *= tensor1
# print(tensor.F.transpose(0, 1))

print("======== Test per-batch operation ========")

coords_batch0 = tensor.coordinates_at(0)
voxels_batch0 = torch.FloatTensor([-500.0]).expand(coords_batch0.size(0), -1)
coords_batch1 = tensor.coordinates_at(1)
voxels_batch1 = torch.FloatTensor([500.0]).expand(coords_batch1.size(0), -1)

# coords2, voxels2 = ME.utils.sparse_collate([coords_batch0, coords_batch1], [voxels_batch0, voxels_batch1])

voxels_expanded = torch.cat([voxels_batch0, voxels_batch1], dim=0)
tensor_per_batch_operation = ME.SparseTensor(features=voxels_expanded,
                          coordinate_manager=tensor.coordinate_manager,
                          coordinate_map_key=tensor.coordinate_map_key)

tensor += tensor_per_batch_operation
print(tensor.F.transpose(0, 1))
# => tensor([[-380.0000, -345.0000, -495.6500, -400.0000, -300.0000,  503.0000, 750.0000,  512.0000,  506.0000]])
print("Done")

기억하면 좋을 것들

  • Sparse tensor는 .C로 할당돼있는 (Batch, x index, y index, z index)인 coordinates와 .F로 할당돼있는 feature 값으로 구성되어 있다.
  • ME.utils.sparse_collate를 통해 여러 sparse tensor를 하나로 합치면, 자연스레 batch에 대한 관리를 할 수 있다
  • In-place operation을 하려면 위와 같이 coordinate_managercoordinate_map_key를 같이 입력으로 주어야 한다.
    • 이는 내부적으로 두 sparse tensor가 같은 key를 사용하고 있는지 빠르게 판단한 후 각 key에 대응하는 value에 대해 연산을 하려고 하는 것이 아닌가로 유추된다.

결론

MinkowskiEngine을 보다 잘 이용하려면 C++의 unordered_map과 같이 key-value로 이뤄져 있는 자료구조를 이해하고 있으면 좋다. 왜냐하면 위의 코드에서 뜬금 없이 coordinate_manager=tensor.coordinate_manager, coordinate_map_key=tensor.coordinate_map_key와 같이 값을 받고 있는데, 현실적으로 이는 key-value로 이뤄진 자료구조를 이해하고 있어야만 이해할 수 있는 부분이다.