GUI 프로그램에서 멀티쓰레딩을 이용해 머신러닝 모델을 이용할 때 유의할 점

일반적으로 머신러닝 모델을 이용하게 되면 evaluation을 하는 데 (실시간 모델이 아니라면) 수 초에서 수십 초 정도가 걸립니다. 커맨드 라인이면 문제가 없지만, GUI 프로그램과 연동을 시킬 경우 프로그램이 그동안 “응답 없음” 상태가 되어 사용자가 혼동을 느낄 수 있습니다. 이 문제를 해결하기 위해 별도의 쓰레드에서 모델을 구동시킬 경우, 한 가지 유의할 점이 있습니다.

보통 메인 쓰레드에서 모델을 불러오고, 아래 코드와 같이 detection을 하는 과정만 (Threading, Multiprocess, Concurrent.futures 등을 이용해) 별도의 쓰레드에서 실행하게 되면, 모델과 관련된 문제가 생깁니다.

    model = Model.SampleModel()
    model.load_weights(FROM_SOMEWHERE)

    ...

    threading.Thread(target=model.detect, args=(inputs, outputs, etc), daemon=True).start()

그 이유는, 모델의 weight 정보 등이 쓰레드간에 공유되지 않기 때문입니다. Detect를 할 때 매 번 모델을 다시 불러오는 방법도 있지만, 그렇게 하면 불필요하게 GPU 메모리를 낭비하고 시간도 더 오래 걸립니다. 이 문제를 해결하려면, 모델 자체를 별도의 쓰레드에서 불러오고 thread lock을 이용해서 detect를 하게 만들면 됩니다.

def create_thread(inputs, outputs, etc, eval_event:threading.Event):
    # 모델 불러오기
    model = Model.SampleModel()
    model.load_weights(FROM_SOMEWHERE)
    while True:
        # Threading.Event로 대기시키기
        eval_event.wait()
        # Lock이 풀리면 detect
        model.detect(inputs, outputs, etc)
        # 다시 False로 설정
        eval_event.clear()
    
exit_event = threading.Event()
# Daemon 설정을 하면 main thread가 죽으면 같이 죽는다. 더 좋게 고치려면 쓰레드 내부에서 종료 로직을 추가로 넣으면 된다.
eval_thread = threading.Thread(target=create_thread, args=(inputs, outputs, etc, eval_event), daemon=True)
eval_thread.start()

일단 create_thread 내부에서는 모델을 불러오고, 무한루프 내부에서 Event를 이용해 대기합니다. 외부에서 eval_event.set()을 이용해 Lock을 풀어주면 detection이 1회 일어나고, 다시 eval_event가 False가 되어 lock이 걸리게 됩니다. 모델 내부에서는 detection이 끝나면 이런 저런 방법들(다른 Event를 쓰거나, 다른 함수를 라이브러리가 제공하면 그걸 쓰는 등)을 이용해 메인 쓰레드에서 detect된 값을 읽어올 수 있게 만들면 됩니다.

더 상세한 조정을 하려면, 여러 Event / Semaphore / Lock / Timer 등을 변수로 넘겨주고, Consumer-Producer pattern 등을 참조해서 쓰레드의 실행을 잘 통제하면 됩니다. 파이썬의 특성상 ‘진짜’ 멀티쓰레드는 아니라고 하지만, 어차피 대부분의 일은 GPU에서 일어나므로 큰 문제가 되지 않았습니다.

*****
긍정적인 영향을 주는 사람이 됩시다
Served using home raspberry pi 3 B+
☕ Pudhina theme by Knhash 🛠️