본문 바로가기
고민의 흔적../AI Agent 행동예측 해커톤

(연구일지 #4 채점 환경 분석)Dacon AI Agent 행동 예측 대회

by sungjinp0805 2026. 7. 8.

좋은 모델보다 먼저, 서버에서 끝까지 도는 코드가 필요했다

이전 글에서는 고전 ML 이후 트랜스포머를 도입한 과정을 정리했다.
klue/roberta-base에 메타 피처를 결합한 하이브리드 모델을 만들었고, 고전 ML 모델들과 스태킹하면서 다음 점수까지 도달했다.

v11 holdout: 0.7006
v11 LB:      0.6849
 

이 시점부터는 모델 구조만 고민하면 되는 줄 알았다.
holdout이 좋아지는 모델을 만들고, 그 모델을 제출하면 리더보드도 같이 오를 것이라고 생각했다.

 

하지만 실제로는 그렇지 않았다.

코드 제출 대회에서는 로컬에서 잘 학습되고 잘 예측되는 모델이라도, 대회 서버에서 제한 시간 안에 끝까지 실행되지 않으면 의미가 없다.


이 구간에서는 모델 성능보다 먼저, 제출 서버에서 안정적으로 돌아가는 코드를 만드는 문제가 계속 발목을 잡았다.

이번 글에서는 v12부터 v18까지, 성능 개선과 별개로 제출 환경에서 겪었던 문제들을 정리한다.


실험 로그

버전          핵심 변경                                                           holdout         LB                     비고
v12 base 모델 + 풀 트랜스크립트 입력 약 0.70 0.6921 단일 모델로 v11 스태킹을 넘어섬
v14 large 모델 도입 0.7129 불안정 서버에서 추론 시간 문제 발생
v15 large 추론 최적화 시도
0.6540
실패 반복 시간 초과, 미완주
v15f fp16 추론 수정 - 0.7158 net.half() 적용 후 large 부활
v16 base 계열 빠른 추론 확인 0.7302 0.7207 large 속도 문제 원인 추적 계기
v18 base + large + 고전 ML 결합 0.7390 0.7200 성능은 높았지만 서버 제약 영향

이 구간의 핵심은 단순히 “모델을 더 좋게 만들자”가 아니었다.

좋은 모델을 만들더라도 서버에서 시간 초과가 나거나, 라이브러리 버전이 맞지 않거나, 모델 용량 제한에 걸리면 제출은 실패한다.
그래서 성능 개선과 동시에 제출 환경을 계속 맞춰야 했다.


v12: 입력 직렬화부터 다시 보기

v10, v11에서는 트랜스포머를 사용했지만, history를 충분히 잘 활용하고 있는지는 다시 확인할 필요가 있었다.

초기에는 history 안의 행동 이름을 토큰처럼 넣는 방식에 가까웠다.
예를 들어 이전에 어떤 action이 있었는지는 반영했지만, 그 action의 구체적인 인자나 결과까지 충분히 담지는 못했다.

 

그런데 AI Agent의 다음 행동은 단순히 “직전에 read_file을 했다” 정도만으로 결정되지 않는다.
어떤 파일을 읽었는지, 어떤 명령을 실행했는지, 실행 결과가 어땠는지, 그 뒤에 사용자가 무엇을 요청했는지가 함께 중요하다.

그래서 v12에서는 입력 직렬화 방식을 바꿨다.

 

기존처럼 action 이름만 넣는 대신, history를 하나의 대화록처럼 풀어냈다.

[U] 사용자 발화
[A] assistant action 이름 + args + 결과
[U] 다음 사용자 발화
...
[CUR] current_prompt
 

즉, 모델이 이전 대화 흐름과 action의 구체적인 내용을 텍스트로 볼 수 있도록 만든 것이다.

이 변경의 효과는 컸다.

v12 holdout: 약 0.70
v12 LB:      0.6921
 

v11에서 고전 ML과 하이브리드 모델을 스태킹해 얻은 LB가 0.6849였는데, v12는 단일 base 모델만으로 이를 넘어섰다.

이 결과를 보고 입력 형식이 모델 구조만큼 중요하다는 것을 확인했다.
트랜스포머를 사용하더라도, 어떤 정보를 어떤 순서와 형태로 넣는지가 성능에 직접적인 영향을 줬다.


truncation 방향 문제

입력을 풀 트랜스크립트 형태로 바꾸자 새로운 문제가 생겼다.

history가 긴 세션에서는 입력 길이가 모델의 최대 길이를 넘어갔다.
이 경우 tokenizer가 일부 토큰을 잘라내야 한다.

 

처음에는 기본 truncation을 사용했는데,

HuggingFace 토크나이저의 기본값이 truncation_side="right"로 되어 있었다.

 

이번 문제에서 가장 중요한 정보는 뒤쪽에 있었다.

현재 프롬프트와 최근 대화가 다음 행동 예측에 직접적으로 연결되기 때문이다.

 

예를 들어 오래전 대화보다, 직전 action과 current_prompt가 더 중요하다.
그런데 뒤쪽이 잘리면 모델이 정작 가장 중요한 현재 요청을 제대로 보지 못할 수 있다.

그래서 truncation 방향부터 바꿨다.

tokenizer.truncation_side = "left"

제출 서버에서 터진 버전 문제들

v12 이후부터는 로컬 실험과 제출 서버의 차이도 계속 문제가 됐다.

로컬에서는 정상적으로 돌아가던 코드가 서버에서는 라이브러리 버전 차이로 실패했다.

대표적으로 numpy 관련 문제가 있었다.

 

pickle 파일 안에 포함된 RNG 객체 때문에 서버에서 PCG64 BitGenerator 관련 에러가 발생했다.
로컬 환경과 서버 환경의 numpy 버전이 달라 생긴 문제였다.

 

해결은 단순했다.

pickle 안에 굳이 필요하지 않은 RNG 객체를 제거하고, requirements에서 버전을 고정했다.
모델 예측에 필요하지 않은 객체는 저장물에 포함하지 않는 쪽이 안전했다.

 

토크나이저에서도 문제가 있었다.

새 버전의 transformers/tokenizers에서 저장한 tokenizer 파일이 서버의 구버전 환경과 맞지 않아 TokenizersBackend 관련 에러가 발생했다.
결국 tokenizer 저장 포맷을 구버전에서도 읽을 수 있는 형태로 패치해야 했다.

 

이때 느낀 점은 명확했다.

코드 제출 대회에서는 모델 파일만 중요한 것이 아니다.
모델, tokenizer, pickle, requirements, 저장 포맷이 모두 서버 환경과 맞아야 한다.


v14: large 모델 도입과 시간 초과

v12 base 모델에서 가능성을 확인한 뒤, 더 큰 모델도 실험했다.


목표는 단순했다.

base 모델보다 표현력이 큰 large 모델을 사용하면, history와 current_prompt의 복잡한 문맥을 더 잘 잡을 수 있을 것이라고 봤다.

v14에서 large 모델의 로컬 holdout 성능은 좋았다.

v14 holdout: 0.7129
 

문제는 제출 서버였다.

로컬에서는 성능이 좋아 보였지만, 서버에서는 추론 시간이 너무 길어졌다.


제한 시간 안에 전체 테스트 데이터를 처리하지 못하면서 제출이 불안정해졌다.

이후 v15에서는 추론 시간을 줄이기 위해 여러 최적화를 시도했다.

 

먼저 입력 길이에 따라 데이터를 정렬해 비슷한 길이끼리 배치로 묶었다.
짧은 입력과 긴 입력이 같은 배치에 섞이면 padding이 늘어나고, 그만큼 불필요한 계산이 많아진다.


길이 정렬 배칭을 적용하면 padding 낭비를 줄일 수 있다.

또한 배치 크기를 조정하고, 남은 시간에 따라 더 가벼운 모델이나 fallback 예측으로 넘어가는 deadline cascade도 구성했다.

 

하지만 large 모델은 여전히 기대보다 느렸다.


fp32 업캐스트 버그

large 모델이 왜 이렇게 느린지 처음에는 명확하지 않았다.
모델이 커졌기 때문에 느려지는 것은 당연했지만, 체감 속도는 예상보다 훨씬 느렸다.

결정적인 계기는 v16이었다.

 

base 계열 모델이 서버에서 약 2분 만에 끝나는 것을 보고, large 모델의 속도가 비정상적으로 느리다는 생각이 들었다.
단순히 모델 크기 차이라고 보기에는 차이가 너무 컸다.

확인해보니 문제는 dtype이었다.

 

fp16으로 저장한 모델이라도, from_pretrained로 로드하는 과정에서 fp32로 올라가는 경우가 있었다.
즉, 저장은 가볍게 해두었지만 실제 추론은 fp32로 돌고 있었던 것이다.

그래서 모델 로드 후 명시적으로 half precision을 적용했다.

 
net.half()
 

이 한 줄이 large 모델 추론 속도를 크게 바꿨다.

fp16 수정 후 large 모델은 다시 제출 가능한 수준으로 돌아왔고, v15f에서 다음 점수를 얻었다.

 

v15f LB: 0.7207
 

이 구간에서 가장 큰 교훈은, 모델이 fp16으로 저장되어 있다고 해서 실제 추론도 fp16으로 돈다고 가정하면 안 된다는 것이었다.
제출 환경에서는 dtype을 직접 확인하고, 필요한 경우 명시적으로 맞춰야 했다.


모델 용량과 RAM 제한

시간 문제를 해결해도 끝이 아니었다.
이번에는 모델 용량과 메모리 제한이 있었다.

제출 파일 용량은 1GB 제한이 있었고, 서버 RAM도 제한되어 있었다.


여러 모델을 한 번에 넣고 싶어도, 저장 용량과 실행 중 메모리를 모두 고려해야 했다.

먼저 모델 저장 용량을 줄이기 위해 fp16 저장을 적용했다.

443MB → 213MB
 

모델 하나의 용량을 줄이는 것만으로도 제출 패키지 구성에 여유가 생겼다.

하지만 여러 모델을 동시에 메모리에 올리면 RAM 제한에 걸릴 수 있었다.
그래서 다중 모델을 사용할 때는 한 번에 모두 로드하지 않고, 순차적으로 로드해 예측한 뒤 바로 해제하는 방식으로 바꿨다.

del model
gc.collect()
 

GPU를 쓰는 환경이라면 cache 정리까지 신경 써야 하지만, 제출 서버 환경에서는 CPU/RAM 사용량을 줄이는 것만으로도 중요했다.

이 방식은 코드가 조금 복잡해지지만, 제한된 서버 환경에서 여러 모델을 사용하는 데 필요했다.


v18: 성능과 서버 제약 사이의 균형

v18에서는 base 모델, large 모델, 고전 ML 모델을 함께 사용하는 방향을 시도했다.

로컬 holdout은 높았다.

v18 holdout: 0.7390
 

하지만 LB는 기대만큼 오르지 않았다.

v18 LB: 0.7200
 

로컬 점수가 높아도 서버에서의 추론 안정성, 시간 제한, 모델 조합 방식, 테스트 분포 차이가 모두 영향을 줄 수 있었다.

이 시점에서 단순히 모델을 많이 넣는 것이 항상 좋은 전략은 아니라는 점을 확인했다.


좋은 모델을 추가하더라도 서버에서 느려지거나, 제한 시간 때문에 일부 fallback이 발생하거나, 테스트셋 분포와 맞지 않으면 기대한 만큼 LB가 오르지 않을 수 있다.

즉, 이 구간부터는 모델 성능과 제출 안정성을 함께 봐야 했다.


이 구간에서 정리한 제출 전략

v12부터 v18까지의 시행착오를 거치면서 제출 코드를 만들 때 확인해야 할 기준이 생겼다.

 

첫째, 입력에서 현재 프롬프트와 최근 history가 잘리지 않도록 해야 한다.
긴 입력을 사용하는 모델에서는 truncation 방향이 중요하다.

 

둘째, 로컬 환경과 서버 환경의 라이브러리 버전 차이를 전제로 두고 저장물을 만들어야 한다.
pickle, tokenizer, requirements는 모두 서버 기준으로 검증해야 한다.

 

셋째, dtype을 직접 확인해야 한다.
fp16 저장만으로는 충분하지 않고, 실제 로드 후 모델이 fp16으로 추론되는지 확인해야 한다.

 

넷째, 추론 시간은 모델 구조만큼 중요하다.
길이 정렬 배칭, 배치 크기 조정, deadline cascade 같은 장치가 필요했다.

 

다섯째, 여러 모델을 사용할 때는 용량과 RAM을 함께 고려해야 한다.
모델을 순차 로드하고, 예측 후 즉시 해제하는 방식이 필요했다.


현재까지의 정리

이 구간의 성능 흐름은 다음과 같았다.

v11 LB:   0.6849
v12 LB:   0.6921
v15f LB:  0.7207
v18 LB:   0.7200
 

v12에서는 입력 직렬화를 풀 트랜스크립트 형태로 바꾸면서 단일 모델 성능이 크게 올랐다.
v14 이후에는 large 모델을 도입했지만, 서버 추론 시간 문제로 여러 번 실패했다.
v15f에서 fp32 업캐스트 문제를 해결하면서 large 모델을 다시 살릴 수 있었다.

 

이 과정에서 얻은 가장 큰 교훈은 간단했다.

로컬에서 좋은 모델이 곧 좋은 제출은 아니다.
코드 제출 대회에서는 모델이 서버에서 제한 시간 안에, 제한된 용량과 메모리 안에서, 같은 라이브러리 환경으로 끝까지 실행되어야 한다.

 

다음 글에서는 다시 모델링 쪽으로 돌아가, 성능을 한 단계 더 끌어올린 데이터 분석과 앙상블 과정을 정리하려고 한다.
특히 특정 세션 집단의 분포 차이, [SRC] 프리픽스, 장기학습 seed 앙상블이 어떻게 LB 0.7701까지 이어졌는지를 다룰 예정이다.