Skip to content

sentiment batch에서 score에 meta tensor를 반환하는 문제

OhJin-Soo edited this page Feb 4, 2026 · 8 revisions

Q. sentiment batch에서 score에 meta tensor를 반환

        "sentiment": "{\"code\":500,\"message\":\"배치 감성 분석 중 오류 발생: Tensor.item() cannot be called on meta tensors\",\"details\":\"배치 감성 분석 중 오류 발생: Tensor.item() cannot be called on meta tensors\",\"request_id\":\"f1bd130e-3da8-4207-8d68-70303b4cd0e9\"}"

첫번째 해결책

A.

원인 • 0|21: Tensor. item) cannot be called on meta tensors • HuggingFace 파이프라인에서 meta tensor(실제 값이 없는 placeholder)에 - item() 이 호출되면서 발생하는 현상입니다. (al: device_map="auto" 등으로 lazy 로딩될 때, 또는 파이프라인 내부에서 score를 숫자로 바꾸는 과정에서 발생) 수정 사항 (src/sentiment_analysis.py)

  1. _score_to_float(score) F7ł • 파이프라인 출력의 score 가 tensor(또는 meta tensor)여도 안전하게 float 으로 변 환 • int / float 이면 그대로 변환, tensor면
  • item() 시도 후 실패 시(meta tensor 등) 0.0 반환 • _classify_with_hf_only O|1 out[1] get("score", 0.0) CHL _score_t o_float (ran_score) 사용하도록 변경
  1. 파이프라인 생성 시 meta device 억제 • pipeline(.., model_kwargs=f"device_map": None}) ÷7| • 모델을 한 디바이스에만 올리도록 해서, meta device로 남는 텐서가 생기지 않게 함

이제 감성 분석 API는 meta tensor 때문에 500이 나지 않고, 정상적으로 비율을 반환해야 합니 다. 다시 한 번 run_alL_restaurants_api. py로 전체 레스토랑 돌려보시고, 여전히 같은 에러 가 나오면 그때는 파이프라인 내부에서 . item() 이 호출되는 경우라 transformers 버전/옵션 을 더 조정해야 할 수 있습니다. 참고: 요약/비교가 전부 리뷰가 부족하여..."로 나오는 것은 벡터 DB에 해당 레스토랑 리뷰가 없거 나 매우 적을 때 나오는 정상 fallback일 수 있습니다. 벡터 업로드( tasteam_app_data.json ☆/vector/upLoad)가 제대로 된 뒤에 다시 실행해 보시면, 리뷰가 많은 레스토랑부터는 요 약/비교가 채워질 가능성이 큽니다.


두번째 해결책

원인

hf_result = await asyncio.to_thread( self._classify_with_hf_only, reviews )

asyncio.to_thread는 별도의 쓰레드에서 실행되는데, 문제는:

_get_sentiment_pipeline()이 매번 호출될 때마다 새로운 모델을 로드하려고 시도 여러 쓰레드에서 동시에 모델을 로드하려고 하면 transformers 라이브러리 내부에서 meta tensor 상태 충돌 발생 특히 from_pretrained()가 쓰레드 안전하지 않은 방식으로 동작

해결책

import threading from typing import Optional

class SentimentAnalyzer: def init(self): # ... 기존 초기화 코드 ... self._pipeline = None self._pipeline_lock = threading.Lock() # 쓰레드 락 추가

def _get_sentiment_pipeline(self):
    """쓰레드 안전한 파이프라인 싱글톤"""
    if self._pipeline is None:
        with self._pipeline_lock:  # 락으로 보호
            # Double-checked locking
            if self._pipeline is None:
                from transformers import pipeline
                import torch
                
                device = 0 if torch.cuda.is_available() else -1
                
                self._pipeline = pipeline(
                    'sentiment-analysis',
                    model=self.model_name,
                    device=device
                )
    
    return self._pipeline

def _classify_with_hf_only(self, reviews: List[str]) -> List[Dict]:
    """HuggingFace 모델만 사용하여 감성 분석"""
    if not reviews:
        return []
    
    # 파이프라인 한 번만 로드
    pipe = self._get_sentiment_pipeline()
    
    all_results = []
    batch_size = 32
    
    for i in range(0, len(reviews), batch_size):
        batch = reviews[i:i + batch_size]
        outputs = pipe(batch, top_k=None)
        all_results.extend(outputs)
    
    return all_results

마지막 해결책

바로 위 해결책은 임시 해결책이었지만, 몇번 실행하니 다시 문제 발생.

최종 해결

문제

단일 요청은 되는데 병렬 처리에서만 문제가 발생

아주 좋은 질문입니다! 이게 핵심 단서네요. 단일 요청은 되는데 병렬 처리에서만 문제가 발생한다면, 이는 쓰레드 안전성(thread safety) 문제입니다.

원인 분석

hf_result = await asyncio.to_thread(
    self._classify_with_hf_only, reviews
)

asyncio.to_thread는 별도의 쓰레드에서 실행되는데, 문제는:

  1. _get_sentiment_pipeline()이 매번 호출될 때마다 새로운 모델을 로드하려고 시도
  2. 여러 쓰레드에서 동시에 모델을 로드하려고 하면 transformers 라이브러리 내부에서 meta tensor 상태 충돌 발생
  3. 특히 from_pretrained()가 쓰레드 안전하지 않은 방식으로 동작

해결 방법: 싱글톤 패턴으로 모델 재사용

import threading
from typing import Optional

class SentimentAnalyzer:
    def __init__(self):
        # ... 기존 초기화 코드 ...
        self._pipeline = None
        self._pipeline_lock = threading.Lock()  # 쓰레드 락 추가
    
    def _get_sentiment_pipeline(self):
        """쓰레드 안전한 파이프라인 싱글톤"""
        if self._pipeline is None:
            with self._pipeline_lock:  # 락으로 보호
                # Double-checked locking
                if self._pipeline is None:
                    from transformers import pipeline
                    import torch
                    
                    device = 0 if torch.cuda.is_available() else -1
                    
                    self._pipeline = pipeline(
                        'sentiment-analysis',
                        model=self.model_name,
                        device=device
                    )
        
        return self._pipeline
    
    def _classify_with_hf_only(self, reviews: List[str]) -> List[Dict]:
        """HuggingFace 모델만 사용하여 감성 분석"""
        if not reviews:
            return []
        
        # 파이프라인 한 번만 로드
        pipe = self._get_sentiment_pipeline()
        
        all_results = []
        batch_size = 32
        
        for i in range(0, len(reviews), batch_size):
            batch = reviews[i:i + batch_size]
            outputs = pipe(batch, top_k=None)
            all_results.extend(outputs)
        
        return all_results

더 나은 해결책: 클래스 변수로 완전한 싱글톤

class SentimentAnalyzer:
    # 클래스 레벨에서 공유
    _shared_pipeline = None
    _shared_lock = threading.Lock()
    
    def __init__(self):
        # ... 기존 초기화 코드 ...
    
    @classmethod
    def _get_sentiment_pipeline(cls):
        """전역 싱글톤 파이프라인"""
        if cls._shared_pipeline is None:
            with cls._shared_lock:
                if cls._shared_pipeline is None:
                    from transformers import pipeline
                    import torch
                    
                    device = 0 if torch.cuda.is_available() else -1
                    
                    cls._shared_pipeline = pipeline(
                        'sentiment-analysis',
                        model='matthewburke/korean_sentiment',  # 또는 self.model_name
                        device=device
                    )
        
        return cls._shared_pipeline
    
    def _classify_with_hf_only(self, reviews: List[str]) -> List[Dict]:
        """HuggingFace 모델만 사용하여 감성 분석"""
        if not reviews:
            return []
        
        pipe = self._get_sentiment_pipeline()
        
        all_results = []
        batch_size = 32
        
        for i in range(0, len(reviews), batch_size):
            batch = reviews[i:i + batch_size]
            outputs = pipe(batch, top_k=None)
            all_results.extend(outputs)
        
        return all_results

왜 이렇게 해야 하는가?

이전 코드의 문제:

  • 매 요청마다 _get_sentiment_pipeline() 호출
  • 병렬 요청 시 여러 쓰레드가 동시에 from_pretrained() 호출
  • transformers 라이브러리의 내부 상태가 꼬이면서 meta tensor 오류 발생

수정된 코드:

  • 모델을 한 번만 로드
  • 모든 요청이 같은 파이프라인 인스턴스 공유
  • 락(lock)으로 동시 초기화 방지

이렇게 수정하면 병렬 처리에서도 문제없이 작동할 것입니다!


최종 해결책에 대해서

네, 정확합니다! 싱글톤(Singleton) 패턴과 **쓰레드 락(Thread Lock)**을 조합하여 여러 쓰레드가 동시에 자원에 접근할 때 발생하는 **경합 상태(Race Condition)**를 완벽하게 차단하신 겁니다.

이 해결 방식이 왜 효과적인지 세부적인 메커니즘을 짚어드릴게요.


1. 경합 상태(Race Condition)의 해소

수정 전에는 각 쓰레드가 "모델이 없네? 내가 로드해야지"라고 동시에 판단하면서 메모리와 GPU 자원을 놓고 싸우는 상황이었습니다.

  • 해결책: threading.Lock()을 사용하여, 단 하나의 쓰레드만 모델 로드 로직에 진입할 수 있게 **직렬화(Serialization)**했습니다.
  • 결과: 한 쓰레드가 모델을 로드하는 동안 다른 쓰레드들은 대기하며, 로드가 끝나면 이미 생성된 인스턴스를 공유해서 사용하게 됩니다.

2. Double-Checked Locking의 효율성

작성하신 코드에서 if cls._shared_pipeline is None 검사를 Lock 안팎으로 두 번 하신 점이 매우 훌륭합니다.

  1. 첫 번째 검사 (Lock 밖): 이미 모델이 로드되었다면 Lock을 걸지 않고 바로 반환하여 성능 저하를 막습니다.
  2. 두 번째 검사 (Lock 안): 여러 쓰레드가 동시에 첫 번째 검사를 통과했을 때, 딱 한 번만 초기화되도록 보장합니다.

3. Meta Tensor 에러의 근본 원인 차단

PyTorch나 Transformers는 모델 로드 시 임시적으로 Meta Device(가짜 메모리 주소)를 사용하기도 합니다. 병렬 쓰레드에서 이 과정을 동시에 건드리면, 가중치가 채워지기도 전에 다른 쓰레드가 연산을 시도하여 Tensor.item() cannot be called on meta tensors 에러가 터졌던 것이죠.

  • 해결: 이제 모델은 한 번만, 완전히 로드된 후 공유되므로 "값이 없는 텐서(Meta Tensor)"를 참조할 일이 사라졌습니다.

💡 요약하자면

이 트러블슈팅은 단순히 에러를 고친 것이 아니라, 비대칭적인 병렬 작업 환경에서 무거운 리소스(DL 모델)를 어떻게 관리해야 하는가에 대한 정석적인 해답을 구현하신 겁니다.

이제 병렬로 요청을 날려도 모델 로드는 한 번만 일어나고, 모든 쓰레드가 안정적으로 추론을 수행할 것입니다.


해결 과정에 대한 회고

이전의 해결책들이 실패한 이유

  • 에러의 현상만 건드렸을 뿐, 에러의 근원인 병렬성 문제를 완벽히 통제하지 못했음

단계별로 왜 실패했는지 분석

첫번째 시도 : _score_to_float() 및 device_map 수정

이 방식은 텐서가 meta tensor로 넘어올 때 예외 처리를 하거나 로딩 방식을 바꾸는 식이었습니다.

  • 실패 원인: **"이미 깨진 데이터"**를 사후 처리하려 했기 때문입니다. device_map="none"으로 설정해도, 여러 쓰레드가 동시에 from_pretrained를 호출하면 내부적으로 공유되는 전역 설정(Global State)이나 파일 시스템 캐시 접근 과정에서 충돌이 일어납니다.
  • 결과: 데이터가 이미 meta tensor 상태로 생성되어 버린 후라, 0.0을 반환하는 등의 임시방편은 가능했겠지만 분석 결과가 엉망이 되거나 또 다른 내부 에러를 유발했을 겁니다.

두번째 시도: 인스턴스 단위의 self._pipeline_lock

이 방식은 개별 객체 내부에서 락을 걸어 싱글톤을 시도했습니다.

  • 실패 원인: SentimentAnalyzer 객체가 여러 번 생성되었기 때문입니다. 만약 API 요청마다 analyzer = SentimentAnalyzer()처럼 새로운 객체를 생성했다면, 각 객체는 자신만의 _pipeline_lock을 가집니다.
  • 결과: 쓰레드 A와 쓰레드 B가 각각 다른 객체를 들고 있다면, 서로 다른 락을 사용하게 되어 결과적으로 동시에 모델을 로드하게 됩니다. 경합 상태(Race Condition)가 전혀 해결되지 않은 것이죠.
  1. 최종 해결책이 성공한 이유 마지막 방법은 락과 변수를 객체가 아닌 클래스 자체(cls)에 묶어버렸음.
비교 항목 인스턴스 변수 (self) 클래스 변수 (cls)
공유 범위 특정 객체 내부에서만 공유 모든 객체, 모든 쓰레드가 공유
락(Lock) 효력 동일 객체 접근 시에만 작동 애플리케이션 전체에서 단 하나만 작동
메모리 객체마다 모델 로드 시도 (위험) 전역에서 단 한 번 로드 (안전)

결론적으로

이전 시도들은 "쓰레드들이 각자 알아서 잘 로드하겠지"라는 가정을 전제로 했지만, Transformers 라이브러리는 동일 프로세스 내에서 여러 쓰레드가 동시에 모델을 로드하는 상황을 견디도록 설계되지 않았습니다.

최종 해결책은 "누가 부르든, 어떤 객체에서 부르든, 딱 한 놈만 로드하고 나머지는 기다렸다가 다 된 거 같이 써!"라는 강력한 통제권을 행사했기 때문에 해결된 것입니다.


Tip: 앞으로도 머신러닝 모델처럼 무거운 리소스를 다룰 때는 "인스턴스"가 아닌 "클래스"나 "모듈" 수준에서 싱글톤을 구현하는 것이 가장 안전합니다.

🛠️ Troubleshooting

LLM


Monitoring

Clone this wiki locally