카테고리 없음

Low Power Computer Vision 후기 / 1위팀 코드 리뷰

낭만가이 2023. 10. 11. 16:27

다른 연구실 홈페이지를 구경하다보니, LPCV (Low Power Computer Vision) 이라는 경진대회에 참여했길래 관심이 생겨 이번에 나도 도전해 보게 되었다.

 

간단하게 얘기하면, 저전력으로 최대한 정확하게 동작하는 컴퓨터 비전 알고리즘을 찾는 경진대회라고 보면 된다.

매년 세부적인 주제는 바뀌는데, 올해(2023년)에는 Jetson Nano (2GB)정도의 저사양 장비에서 드론으로 촬영한 재난 장면을 Semantic segmentation하는 주제로 진행되었다.

 

샘플 이미지 하나만 보면 아래와 같다.

 

대회와 관련한 자세한 내용은 아래를 참고하면 된다.

 

LPCV - Low Power Computer Vision

 

LPCV - Low Power Computer Vision

 

lpcv.ai

데이터는 3월 정도에 공개되었고, 실제 제출은 7월 한달간 하루에 한번씩 해볼 수 있었다. 평가 메트릭은 (Test set mIoU) / (이미지 한장당 inference time)으로 설정되었다. 그리고 최소한 주최측에서 만든 Baseline 모델보다는 더 정확하고, 빨라야 유효한 결과로 인정된다.

 

대회가 본격적으로 진행되는 7월 한달동안 나도 동료들과 같이 열심히 도전해 보았으나... 그리 높은 순위를 얻지는 못하였다. 트레이닝 잘 된 것 같았는데 제출만 하면 점수가 떨어져서... 역시 세계의 벽은 높구나ㅠㅠ

 

다만, 1위 수상팀의 코드가 공개되었길래 살펴본 결과를 한번 정리해 본다.

 

1위 수상팀 코드 리뷰

공개된 코드 깃헙 주소는 아래 링크에 있다. 이사람들이 어떤 회사 소속인지, 아니면 연구실 소속 대학원생인지 잘 모르겠다. 

ModelTC/LPCV_2023_solution (github.com)

 

모델

모델은 DBB (Diverse Branch Block)이라는 구조를 활용했다. 이에 대해서는 다른 포스트에 정리하였다. 

논문읽기: Diverse Branch Block: Building a Convolution as an Inception-like Unit (CVPR 2021) (tistory.com)

간단하게 설명하면, 트레이닝할 때에는 Inception과 같이 여러 브랜치를 가진 모델을 트레이닝하고, Inference시에는 이와 동일한 결과를 내놓는 단순화된 구조를 활용한다. 

 

모델에서 찾아볼 수 있는 몇 가지 특징은 다음과 같다.

  • 파라미터 수나, FLOPS 등을 측정해 보지는 않았지만 Config만 봤을 때, 구조가 생각보다 단순한 것 같지는 않다.
  • Model Config는 다음 링크 참조: 링크
  • Backbone은 Efficientnet과 크게 보면 비슷한데, Inverted Residual이나 SE (Squeeze and Excitation) 등을 쓰지 않고, DBB block으로 대체한 것으로 보인다. 총 6개의 Block이 있다. (코드 링크)
  • Neck은 BiFPN 구조를 활용하였다. 여기에서도 DBB block을 적극적으로 활용하였다. Upsampling은 Transposed Convolution을 활용하였다. (코드 링크)
  • Decoder는 특별한 구조는 없어 보이고, 마지막 레이어와 마지막에서 두번째 레이어로부터 Prediction한 결과를 Training에 활용하였다. (코드 링크)
  • Loss는 Segmentation Ohem(Online Hard Example Mining) Loss를 활용하였다. 트레이닝하면서 Confidence가 높은 영역은 어차피 쉬운 영역이니 제외하고 낮은 영역에 대해서만 Cross Entropy를 이용해서 Loss를 계산하고, Backward pass를 적용하는 것으로 보인다. (코드 링크)
  • 기존 512x512 사이즈의 Input을 256x256으로 줄여서 받도록 되어 있다.

 

Inference 코드 (링크)

사실 위의 설명에서 모델 부분에서 속도 향상에 많은 신경을 쓴 것 같지는 않고, 속도 향상은 대부분 Inference 코드에서 얻어진 것 같다. 여기에서 특이할 만한 점들은 아래와 같다.

  • PyTorch model은 onnx로 변환한 후 TensorRT로 다시 변환하였다. 이 부분은 우리를 포함해서 경진대회에 참여한 많은 팀들이 활용한 것으로 추정된다. 우리의 경우 TensorRT로 변환하였을 때 그냥 Torch를 쓰는 것보다 2배 가량 속도가 빨라졌다.
  • libcudart를 직접 임포트해와서 cuda memory copy나 memory free등을 이곳저곳에서 사용하였다. 그런데 테스트해보니 생각보다 큰 효과는 없는 것 같다. libcudart = ctypes.CDLL('libcudart.so')
  • Inference시 PyTorch Dataset, DataLoader를 활용하였고, Batch size를 4로 사용하였다. 그리고 아래와 같이 여러 Option을 부여하였다. 이부분에서 많은 속도 향상이 있었다.
data_loader = DataLoader(test_dataset, persistent_workers=True, batch_size=4, shuffle=False,
            num_workers=2, prefetch_factor=2, pin_memory=True)

 

Data loader 세팅 바꿔보면서 테스트해본 결과는 아래와 같다.

세팅 수행시간(ms)
제출 버전 (Best) 1.07
pin_memory=False 1.15
batch_size=1 4.34
prefetch_factor 제외 1.23
persistent_workers=False 1.22
num_workers=1 1.08
다른 옵션 없이 batch_size=4만 활용 1.17
아무 옵션도 쓰지 않음. batch_size=1 4.38
우리코드에서 Inference (DataLoader를 쓰지 않음) 4.44

다른 옵션들은 속도에 미치는 영향이 그리 크지 않았으나, batch size를 줄이니 속도가 확 느려졌다. DataLoader를 쓰는 의미가 없다고 봐도 될 정도로 떨어지게 된다.

 

다만, 대회중에 우리도 DataLoader를 써서 테스트를 해 봤었고, 큰 속도 향상이 없었는데 어떻게 이 코드는 4배나 빨라지는지 아직 이해가 되지 않는다. 한가지 의심가는 부분은 아래 코드에서 볼 수 있듯이 Multi Threading을 써서 결과 저장을 하고 있다는 것이다. 

        executor = ThreadPoolExecutor(max_workers=8)
        for input, filenames in data_loader:
            gpu_memcpy_htod(d_input, input.data.numpy().astype(np.float16))
            torch.cuda.synchronize()
            start.record()
            context.execute_v2(bindings=[int(d_input.value), int(d_output.value)])
            end.record()
            gpu_memcpy_dtoh(output_data, d_output)
            time += start.elapsed_time(end)
            outTensor = torch.from_numpy(output_data).cuda().float()
            n, c, h, w = outTensor.shape
            while h < 256:
                h *= 2
                w *= 2
                outTensor: torch.Tensor = F.interpolate(
                    outTensor, (h, w), mode=interp_mode, align_corners=True
                )
            outArray = F.interpolate(
                outTensor, SIZE, mode=interp_mode, align_corners=True
            ).data.max(1)[1]
            outArray = outArray.cpu().numpy().astype(np.uint8)
            executor.submit(write_image, outArray[0], filenames[0])
            executor.submit(write_image, outArray[1], filenames[1])
            executor.submit(write_image, outArray[2], filenames[2])
            executor.submit(write_image, outArray[3], filenames[3])
        executor.shutdown(wait=True)

이부분을 테스트해보기 위해 Multi-threading을 쓰지 않도록 바꿔서 실험을 해 보았다. 놀랍게도 속도가 2배 정도 느려지는 현상을 발견하였다. 코드만 봐서는 시간측정하는 부분이랑 따로 떨어져 있어서 아무 영향이 없어야 할 것 같은데, 어떻게 영향이 있는지 아직 모르겠다.

 

(추가) Github를 통해 수상자에게 질문을 해 보니, GPU Throttling과 관련이 있다고 한다. GPU가 일정 시간 이상 쉬게 되면 Idle 상태로 들어가게 되어서 다시 시작하는데 시간이 더 오래 걸린다고 한다. 이를 방지하기 위해서 시간이 오래 걸리는 Image write는 멀티 쓰레딩으로 처리하고, GPU가 바로 다음 배치에 대한 처리를 하도록 해준 것이라고 한다. ( Github Issue 링크)

 

Multi-threading에 의한 속도 향상을 제외하고라도, 2배 가량 속도 향상이 있었는데 이부분에 대해서도 아직 불분명하다. 이 부분도 질문을 해 보았는데 jtop 같은 걸 이용해서 GPU 사용률을 모니터링해보라는 원론적인 답변만 받았다. 

세팅 수행시간(ms)
제출 버전 (Best) 1.07
Multi-threading 제거, batch_size=4 2.04
Multi-threading 제거, batch_size=2 2.04