Python 내 비동기 및 병렬 처리

인턴일지

목록 보기
5/7
post-thumbnail

Question)

  • 현재 서비스를 비동기적으로 처리할 때, multiprocessing.cpu_count - 2 만큼의 worker를 사용하는 ProcessPoolExecutor 기반 병렬 처리 구조 구성했음.
  • 각 worker가 워커 함수를 수행하면서 서비스를 실행함.
  • CPU 과부화를 막았는데, 같은 어셋에 대해 상이한 OCR 결과물이 나올 수 있을까에 대한 궁금증으로부터 시작됨
  • 과부화는 줄였지만, 결정성(deteminism)은 보장되지 않음.

왜냐???

1. CPU 과부화 방지는 시스템 전체를 보호하는 "스로톨링(Throttling)" 조치일뿐이다.

Throttling이란?

  • CPU, GPU 등이 지나치게 과열될때 기기의 손상을 막고자 클럭과 전압을 강제적으로 낮추거나 강제로 전원을 꺼서 발열을 줄이는 기능
  • AWS 클라우드 서비스에서 지칭할 땐, 서비스 리소스의 사용이 제한되거나 제어되는 것을 의미한다. => 서비스의 안정성을 유지하고 서비스의 과도한 부하를 방지하는데 사용됨.
  • ex) 특정 AWS 서비스에서 고객이 초당 1000개의 API 요청을 수행할 수 있다고 가정해보자. 그러나 어떤 고객이 초당 2000개의 API 요청(요청 max가 1000개로 설정되어 있음)을 하려고 시도한다면, AWS는 이러한 요청을 제한하거나 조절할 수 있음. 이 고객이 초당 1000개 이상의 요청을 계속하려고 하면 AWS는 초과된 요청을 무시하거나 대기시키는 등의 조치를 취할 수 있음.

2. 병렬 처리 순서, 내부 스레드 처리

3. OCR 특성

  • 내부에 ONNX/TensorRT, NumPy 등 병렬 연산 사용 => worker/thread 수에 따라 결과 다름

현재 구조 요약

  • 한 이미지에 대한 OCR 처리는 순차적으로 진행됨
  • 여러 이미지를 리스트로 받고 각 이미지를 병렬 프로세스에서 각각 처리함
    → 즉, 이미지 단위로 병렬화: ProcessPoolExecutor로 parallel inference

+) Python에서 병렬 작업 실행하기

  • 비동기 실행은 (ThreadPoolExecutor를 사용해서) 스레드나 (ProcessPoolExecutor를 사용해서) 별도의 프로세스로 수행함.
  • 그러나, OCR처럼 연산이 많은 작업은 멀티스레딩의 성능이 싱글스레딩보다 떨어짐.
    왜냐하면, 파이썬에서는 GIL 때문에 스레드끼리 공유하는 프로세스의 자원을 Global하게 Lock해버리고 단 하나의 스레드에만 해당 자원에 접근하는 것을 허용한다고 한다.
    → 그래서 멀티스레드라고 하더라도 한 번에 하나의 스레드만 실행함.

+) GIL

  • GIL은 CPython 인터프리터에서 동시성 문제를 방지하기 위한 전역 락으로,
    멀티스레드 환경에서 오직 하나의 스레드만 실행되도록 제한함.
    => Python의 메모리 관리(특히 참조 카운팅 기반 GC)는 스레드 안전하지 않음.
    그래서 동시에 여러 스레드가 객체에 접근해 메모리를 변경하면 문제가 발생할 수 있음.
    이를 방지하기 위해 Python(CPython)은 GIL이라는 전역 락을 도입하여 한 번에 하나의 스레드만 실행되도록 보장함.
    => I/O-bound 작업에는 유리, CPU-bound 작업에서는 성능 저하를 일으킬 수 있음.

+) Worker 함수

  • Worker는 웹서비스에서 백단의 작업을 처리함.
  • 여기서, Python Celery는 메시지를 전달하는 역할(Publisher)과 메시지를 Message Broker에서 가져와 작업을 수행하는 Worker의 역할을 담당하게 된다.
  • 비동기 프로그래밍 처리: 프로그램의 주 실행 흐름을 멈추어서 기다리는 부분 없이 바로 다음 작업을 실행할 수 있게 하는 방식입니다. 즉 코드의 실행 결과를 별도의 공간에 맡겨둔 뒤 결과를 기다리지 않고 다음 코드를 실행하는 병렬처리 방식입니다.

해결 방식

1. OMP, MKL 스레드 제한 -> 멀티스레드 억제

os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
  • 그런데, 현재 구조를 참고해보면,
    • 한 이미지에 대한 OCR 처리는 순차적으로 진행하고 / 여러 이미지 대상으론, 각 이미지를 병렬 프로세스에서 각각 처리해주고 있음.
      => 각 프로세스는 독립된 Python 환경을 띄워서 PaddleOCR 객체 초기화도 각자 따로 이뤄짐.
      (== 한 이미지 내에서 연산은 순차적으로 이뤄지지만, 연산 환경은 워커 함수 환경마다 달라질 수 있다는 뜻)
    • 한 이미지 내부에서 OCR 처리 시, 내부 라이브러리 (Paddle, Numpy 등등)은 다중 스레드를 사용함.
    • 그러면, 굳이 스레드 수를 1개로 고정하지 말고 다중 스레드를 사용하되, 워커 함수 내부에서 다중 스레드를 고정된 수로 선언해주면 되지 않을까?
      • "스레드 수가 1이 아니어도, 시드와 연산 환경을 모두 고정한다면 대부분의 경우 동일한 OCR 결과를 얻을 수 있다. 단, 결정성이 100% 보장되진 않으므로, 테스트 정확도를 최우선하는 경우엔 1로 고정하는 것이 안전하다." 라고 함..

2. 랜덤성 제거 -> random, numpy seed 고정

import random
import numpy as np
import paddle

random.seed(0)
np.random.seed(0)
paddle.seed(0)
  • worker 내부에서 다시 모든 seed와 환경을 고정해줘서 함수 내부에서 병렬 처리를 억제함.

3. enable_mkldnn=False

  • paddleOCR 내부 파라미터로 선언: MKL-DNN 최적화 비활성화

MKLDNN 최적화란?

  • Intel에서 만든 딥러닝 최적화 라이브러리로 수치 연산 빠르게 처리하는 라이브러리 + 딥러닝 레이어 연산 최적화 라이브러리
  • 즉, Intel CPU 환경에서도 딥러닝 연산을 빠르게 실행하게 해주는 고성능 연산 엔진
    [동작 방식]
  • 행렬 곱(MatMul), 합성곱(Conv2D), 활성화 함수(ReLU, Softmax 등) 등의 연산을 CPU 아키텍처에 최적화된 방식으로 SIMD(벡터 연산) 또는 멀티스레딩을 활용해 처리해주는 로직

출처:
https://docs.python.org/ko/3.13/library/concurrent.futures.html
https://tibetsandfox.tistory.com/43

profile
밝은 미래 FE 개발자의 기록

0개의 댓글