CPU에서 도는 스킬 검색: SKILLRET 소형 모델 패밀리 개발기
왜 작은 모델이 필요했나
에이전트에 스킬이 수천 개 붙으면, 매 턴마다 그중 무엇을 프롬프트에 올릴지 먼저 골라야 합니다. 우리는 이 문제를 재기 위해 SkillRet 벤치마크를 만들었고, 0.6B·8B 임베딩 모델도 공개했습니다.
0.6B 모델도 BF16으로 1,191.6MB입니다. 스킬 검색은 턴마다 한 번씩 도는 앞단 작업이라 GPU를 따로 붙이기 아깝습니다. 그래서 질문을 이렇게 잡았습니다. 교사 모델의 검색 품질을 얼마나 남기면서 CPU 한 코어로 돌 만큼 작게 만들 수 있는가.
이 글은 그 과정에서 나온 모델 9개를 다룹니다. 22M·109M 학생 모델, 그 int4·int3 양자화본, 층마다 비트를 다르게 준 drift-mixed, 그리고 2단 검색용 Reranker-0.6B입니다. 305M 모델과 그 INT8 실패는 사전등록 게이트 글에서 다뤘으니 여기서는 반복하지 않습니다.
먼저 밝혀 둘 평가 조건
이 글의 숫자는 세 종류이고, 서로 섞어 비교하면 안 됩니다.
첫째는 학생 모델 평가입니다. SkillRet 테스트 분할(질의 4,392개, 스킬 6,006개, 데이터셋 revision a050ad2)에서 질의 앞에 지시문을 붙이지 않고, CLS 풀링과 L2 정규화, 최대 256토큰, FP32 연산으로 쟀습니다. 지표는 NDCG@10이고 질의 단위 표준오차는 0.42~0.46입니다. 교사(SKILLRET-Embedding-0.6B)도 이 조건에서 78.48입니다.
둘째는 리랭커 평가입니다. 같은 테스트 분할이지만 벤치마크 팀 하네스에서 각 모델의 권장 프롬프트를 쓰고, 8B 임베딩이 뽑은 상위 20개를 다시 정렬했습니다. 이 하네스에서 0.6B는 81.12, 8B는 86.44로 나옵니다. 첫째 조건의 78.48과 숫자가 다른 이유가 이것입니다.
셋째는 CPU 비용입니다. 기계와 입력 길이(최대 128토큰)가 다르므로 품질 표와 같은 줄에 놓지 않았습니다.
초기 모델 카드 몇 개는 교사 점수를 80.82로 잡고 “교사 대비 %”를 계산했습니다. 이 값은 학습과 평가의 지시문이 어긋난 상태에서 나온 것이라 폐기했고, 이 글은 다시 잰 78.48을 기준으로 씁니다.
증류 레시피
학생 모델은 두 크기입니다. 22M은 snowflake-arctic-embed-xs에서, 109M은 snowflake-arctic-embed-m(BERT-base 크기)에서 시작했습니다. 22M은 파라미터의 51.6%가 임베딩 표라서 트랜스포머 층 자체는 아주 얇습니다.
학습은 대조 학습과 지식 증류를 섞었습니다. 정답 스킬이 여러 개인 질의를 위해 multi-positive InfoNCE를 쓰고, 교사의 유사도 분포를 따라가는 KD 손실을 가중치 0.7로 더했습니다. 12에폭을 돌렸고, 에폭 선택은 테스트 분할이 아니라 스킬이 겹치지 않는 별도 홀드아웃에서 했습니다.
지시문 하나가 8.84pp를 움직였다
초기에 한 체크포인트가 양자화하면 오히려 fp16보다 점수가 오르는 것처럼 보였습니다. 따라가 보니 학습 때와 평가 때 질의 앞 지시문이 달랐습니다. 이 불일치 하나로 같은 체크포인트 점수가 8.84pp 움직였습니다. 교사 점수 80.82가 폐기된 것도 같은 원인입니다. 이후 모든 학생은 지시문 없이 학습하고 지시문 없이 평가합니다.
시도했다가 버린 것: LEAF 보조 손실
학생의 임베딩 공간을 교사 공간에 직접 맞추는 LEAF 보조 손실(가중치 0.3)도 넣어 봤습니다. 22M에서 75.26이 73.75로 1.51pp 떨어졌습니다. 같은 질의 집합으로 짝지어 계산한 표준오차는 0.194로, 우연으로 보기 어려운 손해였습니다. 교사 공간에 맞추라는 제약이 검색 목적과 부딪힌 것으로 보고 채택하지 않았습니다.
같은 조건에서 본 크기와 품질
아래는 모두 첫째 조건(지시문 없음, n=4,392)에서 잰 값입니다. 양자화는 그룹 단위 비대칭 min/max이고, 그룹 크기는 따로 적지 않으면 16입니다. 크기는 디스크에 실제로 저장된 패킹 파일 기준입니다. “상위 10개 일치”는 fp16 모델이 낸 상위 10개 스킬 중 몇 %가 양자화 후에도 남는지입니다.
| 모델 | 정밀도 | 크기 | NDCG@10 | 상위 10개 일치 |
|---|---|---|---|---|
| 교사 0.6B | BF16 | 1,191.6MB | 78.48 | 기준 |
| 교사 0.6B | int3/g32 | 297.9MB | 64.46 | 미측정 |
| 22M | fp16 | 45.4MB | 75.26 (±0.45) | 기준 |
| 22M | int8 | 미기재 | 75.27 | 99.2% |
| 22M | int4 | 17.0MB | 75.14 | 89.1% |
| 22M | int3 | 14.2MB | 73.82 (±0.46) | 76.6% |
| 22M | int3/g32 | 미기재 | 71.37 | 미측정 |
| 22M | int2 | 미기재 | 59.42 | 41.2% |
| 109M | fp16 | 219.0MB | 79.18 (±0.42) | 기준 |
| 109M | int8 | 미기재 | 79.18 | 99.3% |
| 109M | int4 | 82.0MB | 79.21 | 90.1% |
| 109M | int3 | 68.4MB | 78.04 (±0.44) | 78.9% |
| 109M | int3/g32 | 54.7MB | 77.10 | 미측정 |
| 109M | int2 | 미기재 | 63.55 | 38.6% |
표에서 읽을 수 있는 것은 네 가지입니다.
첫째, 22M과 109M의 차이는 약 3.9pp입니다. 109M fp16(79.18)은 교사(78.48)보다 0.70 높지만 표준오차 안이라 “교사와 비슷하다”까지만 말할 수 있습니다. 22M fp16은 교사의 95.9%입니다. 크기는 109M이 4.8배 큽니다.
둘째, int4는 NDCG로 보면 거의 손해가 없습니다(22M −0.12, 109M +0.03). 그런데 상위 10개 일치는 89~90%입니다. 평균 점수는 그대로여도 질의 열 개 중 하나꼴로 상위 10개 안의 스킬 하나가 바뀐다는 뜻입니다. 순위 결과를 캐시하거나 회귀 테스트에 고정해 둔 시스템이라면 이 변화를 알고 써야 합니다.
셋째, int3부터 비용이 보이기 시작합니다. 22M은 −1.44pp, 109M은 −1.14pp입니다. 그룹을 32로 키우면 22M에서 −3.89pp까지 벌어집니다. int2는 두 모델 모두 크게 무너집니다(59.42, 63.55).
넷째, 큰 모델을 직접 줄이는 것보다 증류한 뒤 줄이는 편이 나았습니다. 교사를 int3/g32로 줄이면 297.9MB에서 64.46인데, 22M int4는 17.0MB에서 75.14입니다.
drift-mixed: 층마다 다른 비트
같은 바이트로 품질을 더 남길 수 있는지 보려고, 22M에서 층마다 비트를 다르게 주는 실험을 했습니다. 어느 층에 비트를 더 줄지는 라벨 없이 정했습니다. 보정용 스킬 1,500개와 질의 150개를 각 층만 2·3·4비트로 바꿔 통과시키고, 출력 임베딩이 원본에서 얼마나 벗어나는지(1−코사인)를 쟀습니다. 그 표를 놓고 주어진 비트 예산 안에서 벗어남이 가장 작은 조합을 정수 계획으로 골랐습니다.
결과 조합은 임베딩 표에 2~3비트, 중간 어텐션·FFN 블록에 4비트를 주는 모양이었습니다. 이것을 SKILLRET-Edge-22M-drift-mixed로 공개했습니다. 평가는 첫째 조건과 같고, 차이는 같은 질의로 짝지은 부트스트랩 5,000회로 계산했습니다.
| 비교 | 크기 | NDCG@10 | 차이 [95% CI] |
|---|---|---|---|
| drift-mixed (평균 4.99비트) | 14.14MB | 74.56 | 기준 |
| 균일 int3 (5.17비트) | 14.65MB | 73.91 | drift가 +0.65 [+0.35, +0.96] |
| drift, 같은 5.2비트 예산 | 14.73MB | 74.89 | 균일 int3보다 +0.98 [+0.64, +1.33] |
| 라벨로 고른 배분, 같은 예산 | 14.14MB | 73.85 | drift가 +0.71 |
| 무작위 배분 5회, 같은 예산 | 14.14MB | 65.00~66.83 | drift가 +7.7~+9.6 |
| drift, 4.5비트 예산 | 12.74MB | 70.42 | 균일로는 만들 수 없는 크기 |
정직하게 적어야 할 점이 있습니다. 실험 전에 정한 성공 기준은 “같은 바이트에서 균일 int3보다 +1.0pp”였습니다. 같은 바이트에서는 +0.65로 기준에 못 미쳤고, 같은 비트 예산에서 +0.98로 기준선에 가까이 갔습니다. 그래서 이 모델은 “균일 int3보다 작고 조금 낫다”까지만 주장합니다.
그래도 배분 순서에 값이 있다는 것은 분명했습니다. 같은 예산에서 무작위로 나누면 8~9pp가 떨어졌습니다. 벗어남 순서는 보정 데이터를 반으로 나눠도 그대로였지만(스피어만 1.000), 라벨로 매긴 순서는 재현되지 않았습니다(−0.29~+0.34).
이 방법이 늘 이기지는 않습니다. 같은 방법을 다른 임베딩 모델 세 개에 적용한 후속 실험에서는 주요 예산에서 균일 양자화보다 0.85~1.01pp 낮았습니다. drift-mixed는 SKILLRET-Edge-22M에서는 통했고, 일반 기법으로 검증된 것은 아닙니다.
Reranker-0.6B: 조건이 다른 별도 표
두 단계 검색의 두 번째 단계로, Qwen3-Reranker-0.6B를 SkillRet 학습 분할(질의 63,259개, 스킬 10,123개)로 미세조정했습니다. 네 개의 검색기가 21~60위에 올린 스킬을 어려운 오답으로 쓰고, 정답 하나에 오답 15개를 붙여 예/아니오 이진 분류로 학습했습니다. B200 4장으로 1에폭, 최대 길이 8,192토큰입니다.
이 표는 둘째 조건(벤치마크 팀 하네스)에서 잰 값이라 위 학생 표와 비교하면 안 됩니다.
| 지표 | 8B 임베딩만 | 8B 상위 20개 + Reranker |
|---|---|---|
| NDCG@5 | 84.58 | 86.10 |
| NDCG@10 | 86.44 | 87.74 |
| Recall@10 | 93.25 | 93.57 |
| Completeness@10 | 88.11 | 88.96 |
NDCG@10이 1.30 올랐습니다. Recall@10은 0.32만 올랐으니, 리랭커는 이미 상위 10개 안에 있던 정답을 더 위로 올리는 역할을 주로 했습니다.
비용도 같이 봐야 합니다. 후보 20개를 다시 매기는 데 B200 한 장에서 질의당 약 0.8초가 걸렸습니다. CPU용 학생 모델과는 비용 차원이 다릅니다. 그리고 우리는 리랭커를 22M·109M 학생이 뽑은 후보 위에 얹어 잰 적이 없습니다. “학생 + 리랭커가 교사를 넘는다”는 말은 지금 근거가 없습니다.
CPU에서 실제로 얼마나 드나
품질과 별개로, CPU에서 22M int4 모델을 돌리는 비용을 두 가지 경로로 쟀습니다.
첫 번째는 PyTorch 경로입니다. 공개 저장소의 model.safetensors는 int4 값을 fp16으로 되돌려 둔 파일이라 연산은 FP32로 일어납니다. 진짜 4비트 패킹 파일(model-int4-g16.bin)은 따로 들어 있지만, 이 경로에서는 크기 이점만 있고 정수 연산 이점은 없습니다.
| 기계 (4스레드) | 질의 1개 p50 | 32개 배치, 개당 |
|---|---|---|
| Apple M4 Pro | 6.5ms | 5.11ms |
| AMD EPYC 9355 | 3.1ms | 1.25ms |
두 기계는 하드웨어 전체가 다르므로 이 차이를 명령어 집합 효과로 읽으면 안 됩니다. M4 Pro에서 프로세스 최대 상주 메모리는 1,061MB였습니다. 모델보다 런타임이 대부분을 차지합니다.
두 번째는 ONNX Runtime 동적 INT8 경로로, 행렬곱이 실제로 정수 연산으로 돕니다. SkillRet 테스트 질의 512개(평균 272토큰을 128토큰으로 자름)를 세 번 돌린 중앙값입니다. 지표는 질의 하나에 쓴 CPU 시간입니다.
| 기계 | 경로 | 질의당 CPU-ms |
|---|---|---|
| M4 Pro | ORT INT8, 1스레드 | 8.2~9.4 |
| M4 Pro | PyTorch, 1스레드 | 15.6 |
| Xeon Platinum 8592V 파드 | ORT INT8, 1스레드 | 14.5 (코어당 약 69 QPS) |
| Xeon Platinum 8592V 파드 | ORT FP32, 1스레드 | 29.0 |
| Xeon Platinum 8592V 파드 | PyTorch, 1스레드 | 26.9 |
| Xeon Platinum 8592V 파드 | PyTorch, 기본 8스레드 | 74.8 |
정수 연산은 CPU 시간을 1.7~2배 줄였습니다. 더 큰 차이는 스레드 설정에서 났습니다. PyTorch를 기본 8스레드로 두면 질의당 CPU 시간이 74.8ms로, 1스레드 PyTorch의 2.8배, ORT INT8 1스레드의 5.2배였습니다. 원인 하나는 스핀 대기였습니다. ORT를 4스레드로 두면 놀고 있는 1초마다 코어 1.77초를 태웠고, allow_spinning=0이나 OMP_WAIT_POLICY=PASSIVE로 끄면 사라졌습니다. 코어 고정은 CPU 사용량을 줄이지 못했습니다(±2%).
측정하면서 배운 것도 하나 있습니다. 잡 이름에는 EPYC 노드라고 적었는데, 실제로 배정된 노드는 Xeon이었습니다. 이후로는 CPU 모델명을 로그에서 읽어 원장에 남깁니다.
이 ONNX INT8 파일에는 한계가 있습니다. 출력이 PyTorch 경로와 같은지는 코사인 유사도(1.0000)로만 확인했고, SkillRet NDCG는 재지 않았습니다. 그래서 HF에도 올리지 않았습니다.
스킬 검색 밖에서는 약하다
학생 모델은 SkillRet에 맞춰 특화돼 있고 일반 검색으로 옮겨 가지 않습니다. 같은 평가 코드로 BEIR의 SciFact와 NFCorpus를 재 보면 차이가 큽니다(NDCG@10).
| 모델 | SkillRet | SciFact | NFCorpus |
|---|---|---|---|
| 22M fp16 | 75.26 | 55.58 | 16.86 |
| 109M fp16 | 79.18 | 50.17 | 7.54 |
| 교사 0.6B BF16 | 78.48 | 69.43 | 33.86 |
| E5-base-v2 | 51.72 | 71.05 | 35.52 |
| EmbeddingGemma-300M | 60.44 | 78.32 | 38.75 |
109M은 SkillRet에서 가장 높지만 NFCorpus에서 7.54로 가장 낮습니다. 스킬 카탈로그 검색 말고 다른 용도에 쓰려면 이 모델들은 맞지 않습니다.
실패와 철회
2026년 9월 11일, 대칭 양자화로 잰 네 개 체크포인트의 결과를 철회했습니다. 그룹 안 값이 전부 0이면 fp16 스케일이 0으로 떨어져 NaN이 생겼고, 이것이 모델 붕괴처럼 보였습니다. 공개 모델은 모두 비대칭 양자화로 다시 쟀습니다.
int2는 진짜로 무너집니다. 22M 59.42, 109M 63.55로, 크기 이점도 int4보다 크지 않았습니다. 일부 초기 카드에 적힌 “INT2에서 0.1 근처로 붕괴”는 교사 0.6B의 사후 양자화 결과이고 학생 모델 값이 아닙니다.
Paxis와의 연결은 어디까지인가
Paxis 에이전트 서버(praxis 저장소)에는 턴마다 어떤 툴을 프롬프트에 실을지 고르는 툴 스코어러가 있고, 기본값은 TF-IDF입니다. 임베딩 스코어러로 바꾸는 설정이 있고, 그 대상으로 SKILLRET GPU 서빙과 CPU 서빙이 적혀 있습니다. 저장소에 기록된 라벨 43건 측정에서 TF-IDF는 17건, SKILLRET-Embedding-0.6B는 38건을 맞혔습니다.
22M은 9월 29일에 데모 Metis의 CPU vLLM에 올려 지연만 쟀습니다. 캐시된 상태에서 질의 하나가 p50 13ms였고, 동시 8턴 부하에서는 p99 351ms였습니다. 이 측정은 Paxis 쪽 요청 형식(지시문 포함)으로 잰 것이라 위 CPU 표와 조건이 다릅니다. 같은 라벨 43건 벤치는 22M으로 아직 돌리지 않았습니다.
정리하면, 22M을 Paxis 툴 선택에 쓰는 배선과 지연 측정은 있지만, 기본값으로 켜진 것은 아니고 그 자리에서 품질이 검증된 것도 아닙니다. Metis에서 CPU로 서빙하는 경로는 지연 측정 수준까지 확인됐습니다.
정리
| 고르는 기준 | 후보 | 근거 |
|---|---|---|
| 가장 작게, 품질 손해 최소 | 22M int4 (17.0MB, 75.14) | fp16과 0.12 차이, 단 상위 10개의 약 11%가 바뀜 |
| 같은 크기대에서 품질 우선 | 109M int4 (82.0MB, 79.21) | 교사와 같은 수준 |
| 15MB 아래 | 22M drift-mixed (14.14MB, 74.56) | 균일 int3보다 작고 +0.65 |
| 정확도 최우선, GPU 있음 | 8B + Reranker-0.6B (87.74) | 다른 하네스, 질의당 약 0.8초 |
남은 일은 세 가지입니다. 학생 모델 위에 리랭커를 얹은 조합을 같은 조건으로 재는 것, ONNX INT8 파일의 NDCG를 재는 것, Paxis 라벨 벤치를 22M으로 돌리는 것입니다. 이번 글을 쓰면서 새로 학습하거나 다시 평가한 것은 없습니다. 모든 숫자는 기존 원장과 공개 카드에서 가져왔습니다.
공개 모델은 Hugging Face의 ThakiCloud 조직에서 볼 수 있습니다: SKILLRET-Edge-22M, 22M-int4, 22M-int3, 22M-drift-mixed, SKILLRET-Edge-109M, 109M-int4, 109M-int3, 109M-int3-g32, SKILLRET-Reranker-0.6B.