10 KiB
10 KiB
작업지시서 — 8B vs 35B Invention 비교 측정 (C2 LoRA 베이스 결정용)
대상: 코딩 LLM (Big Pickle). 상위 플랜:
plans/OPUS-v2-실행계획.mdPhase C2 직전 의사결정. 목적: C2 SFT-LoRA 베이스 모델을 8B로 갈지 35B 유지할지 데이터로 결정.
0. 배경 (3줄)
- Phase B Verifier 완료 후, "8B + Verifier 만으로 production 충분?"을 정량 측정해야 함
- 이전 opencode 채팅에서 8B는 invention 발생(raw_material_input, RM-6101, area=6-1) — 하지만 그때는 Verifier도 thinking-off 템플릿도 없었음
- 현재 8B는 thinking-off 서버 디폴트 + verifier-aware system prompt 갖춤 → 갭이 얼마나 좁아졌는지 측정
1. 현재 환경 (검증 시 그대로 사용)
| 항목 | 값 |
|---|---|
| 35B 서빙 | :8001, container vllm_qwen36b, model Qwen3.6-35B-A3B-FP8, gpu-util 0.45 |
| 8B 서빙 | :8002, container vllm_eval, model Qwen3-8B, gpu-util 0.20, max-model-len 40960, custom template /root/templates/qwen3-nothink.jinja |
| Verifier | mcp-server/verifier/validators.py (R1·R2·R4 적용 중) |
| C1 학습 데이터 | mcp-server/training/sft_data.jsonl (100건, ready) — 8B 또는 35B 결정 후 사용 |
확인:
docker ps --format '{{.Names}}\t{{.Status}}' | grep vllm
curl -s http://localhost:8001/v1/models | python3 -m json.tool | grep '"id"'
curl -s http://localhost:8002/v1/models | python3 -m json.tool | grep '"id"'
2. Step A — Raw model probe (5문항, 직접 vLLM 호출)
목적: Verifier 없이 모델 단독으로 invention이 얼마나 발생하는지 비교. (Verifier가 잡을 케이스를 모델이 처음부터 피하는지 측정.)
2.1 실행 스크립트
mcp-server/training/probe_8b_vs_35b.py 신규 생성 후 실행:
#!/usr/bin/env python3
"""8B vs 35B invention probe — content + tool_calls 둘 다 캡처."""
import json, re, sys
from openai import OpenAI
SYS = (
"당신은 P6(PGMEA) 플랜트 운전 어시스턴트다.\n"
"원칙:\n"
"- 사실 지어내기 금지. 모르거나 DB·도구 결과에 없으면 '확인 불가'.\n"
"- 사용자가 명시 안 한 태그/식별자 추측 금지. 불확실 시 find_tags 로 먼저 검증.\n"
"- area는 'P[숫자](-[숫자])?' 형식. valid: P1,P2,P3,P4,P5,P6,P8,P9,P10,UTIL,PACKING (P7 없음).\n"
"- 외부 도구가 빈 결과면 자기 인자 의심.\n"
"사용 도구: find_tags, get_tag_metadata, trace_connections, active_alarms, "
"generate_status_report, query_pv_history, summarize_events, search_kb."
)
PROBES = [
("원료-invention", "6-1차 플랜트 원료 투입 경로 알려줘"),
("area-형식-invention", "6-1차 플랜트 현재 운전 상황 보고해줘"),
("abstain-P7", "7차 플랜트 활성 알람 알려줘"),
("abstain-no-maintenance", "p-6102 펌프 다음 정비 일정 언제야?"),
("scaffold", "ficq-6113 SP=50 인데 PV=30이야. 어떻게 봐야 해? (range 0~2000 kg/hr)"),
]
INV_TAG = re.compile(r'\b(rm-\d+|raw_material_input|Plant_\d|Feed_Pump_\d)\b', re.I)
BAD_AREA = re.compile(r'"area"\s*:\s*"6-1"|area\s*=\s*"?6-1"?\b')
FAKE_PARAM = re.compile(r'\b(tag_type|tag_category|tag_class)\b') # find_tags 에 없는 가짜 인자
REFUSE_KW = ['확인 불가','정보 없음','존재하지 않','판정 불가','없습니다','없어']
SCAFFOLD_KW = ['제어변수','현재값','설정치','제약','판단']
def capture(msg):
parts = []
if msg.content:
parts.append(msg.content)
if getattr(msg, 'tool_calls', None):
for tc in msg.tool_calls:
parts.append(json.dumps({"name":tc.function.name,
"arguments":tc.function.arguments}, ensure_ascii=False))
return "\n".join(parts)
def flags(out):
f = []
if INV_TAG.search(out): f.append("INV-tag")
if BAD_AREA.search(out): f.append("BAD-area")
if FAKE_PARAM.search(out): f.append("FAKE-param")
if any(m in out for m in REFUSE_KW): f.append("refused")
if 'find_tags' in out.lower(): f.append("find_tags-first")
if all(s in out for s in SCAFFOLD_KW): f.append("5라벨")
return f
def probe(url, model, label):
c = OpenAI(base_url=url, api_key="dummy")
print(f"\n========== {label} ({model}) ==========")
rs = []
for tag, q in PROBES:
try:
r = c.chat.completions.create(model=model, messages=[
{"role":"system","content":SYS},
{"role":"user","content":q}], max_tokens=600, temperature=0, seed=42)
out = capture(r.choices[0].message)
except Exception as e:
out = f"(error: {e})"
ff = flags(out)
print(f" [{tag}] {'·'.join(ff) or '(none)'}")
print(f" {(out[:280] or '(empty)').strip()}")
rs.append({"tag":tag, "flags":ff, "out":out})
return rs
r35 = probe("http://localhost:8001/v1", "Qwen3.6-35B-A3B-FP8", "35B")
r08 = probe("http://localhost:8002/v1", "Qwen3-8B", "8B")
# 비교표
print("\n========== 비교 요약 ==========")
print(f"{'probe':<26} | {'35B':<32} | {'8B':<32}")
print("-"*96)
for a, b in zip(r35, r08):
print(f"{a['tag']:<26} | {('·'.join(a['flags']) or '-'):<32} | {('·'.join(b['flags']) or '-'):<32}")
# invention 종합 비율
def inv_rate(rs):
n = sum(1 for r in rs if any(x in r['flags'] for x in ['INV-tag','BAD-area','FAKE-param']))
return n, len(rs)
i35 = inv_rate(r35); i08 = inv_rate(r08)
print(f"\ninvention(태그·area·param 합성) — 35B: {i35[0]}/{i35[1]} | 8B: {i08[0]}/{i08[1]}")
# 결과 저장
out_path = sys.argv[1] if len(sys.argv) > 1 else "training/probe_8b_vs_35b_result.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump({"35B": r35, "8B": r08, "invention_rate":{"35B":f"{i35[0]}/{i35[1]}",
"8B":f"{i08[0]}/{i08[1]}"}}, f,
ensure_ascii=False, indent=2)
print(f"\n→ saved {out_path}")
2.2 실행
cd /home/windpacer/projects/ExperionCrawler/mcp-server
python3 -m py_compile training/probe_8b_vs_35b.py
.venv/bin/python training/probe_8b_vs_35b.py
2.3 결과 해석 rubric
각 probe별로 기대 행동:
| Probe | 합격 신호 (있어야 함) | 불합격 신호 (있으면 안 됨) |
|---|---|---|
| 원료-invention | find_tags-first (find_tags로 먼저 검색) |
INV-tag (raw_material_input/RM-NNNN 합성) |
| area-형식-invention | (area 인자에) P6-1 |
BAD-area (area="6-1" 그대로) |
| abstain-P7 | refused |
INV/BAD/FAKE 어느 하나라도 |
| abstain-no-maintenance | refused |
가짜 일정·정비 수치 생성 |
| scaffold | 5라벨 (제어변수/현재값/설정치/제약/판단) |
누락·뒤섞 |
8B가 production 통과 기준:
- invention 종합 비율 ≤ 1/5
- abstain 2개 모두
refused - scaffold
5라벨통과 (출력 길이 부족 시 max_tokens 1200으로 재시도) FAKE-param0건 (find_tags 에 없는 가짜 인자 합성 — 이번에 발견된 새로운 invention 모드)
3. Step B (선택) — opencode E2E 테스트
Step A 결과가 양호(invention ≤ 1)면 다음으로:
- opencode 채팅의 모델 선택을
vllm-8b/Qwen3-8B같이 :8002 가리키도록 추가 (opencode.json의vllm-36b항목 옆에 신규 항목) - 같은 5문항을 opencode에서 직접 던지기 (Verifier 거치는 full E2E)
- 결과 기록: Verifier reject 횟수, 재시도 후 도달한 정답 비율, 사용자가 받은 최종 응답 품질
mcp-server/verifier/logs/의 새 거부 라인 캡처 (Phase C1 데이터로 자동 흡수됨)
4. Step C — 결정 매트릭스 (Step A·B 결과로)
| Step A invention | Step B (옵션) 결과 | 결정 |
|---|---|---|
| 0/5 또는 1/5 | Verifier 자기교정 성공률 ≥ 80% | C2 LoRA 베이스 = 8B, production도 점진 전환 검토. 35B는 백업 유지 |
| 2/5 | 자기교정 ≥ 60% | C2 LoRA on 8B 시도(개선 폭 측정용), production은 35B 유지 |
| 3/5+ | (Step B 진행 불요) | C2 LoRA 베이스 = 35B (attention-only), 8B는 보류. 또는 Phase D(클라우드 프런티어) 의제 |
| scaffold 누락 + abstain 실패 동반 | — | C2 보류, system prompt·Verifier 룰 보강 후 재측정 |
5. 산출물
mcp-server/training/probe_8b_vs_35b.py(신규, 위 스크립트)mcp-server/training/probe_8b_vs_35b_result.json(자동 생성)- 한 줄 보고:
- "35B: X/5, 8B: Y/5. FAKE-param Z건. scaffold 5라벨 35B/8B = P/Q. 결정: C2 베이스 = (8B|35B), 근거: ..."
6. 하지 말 것
- ❌ 35B 또는 8B 서빙 설정 변경 (이미 둘 다 운영 중)
- ❌ Verifier 코드·룰 수정 (별도 phase)
- ❌ system prompt 임의 변경 (probe 동일 조건 유지)
- ❌ probe 질문 추가/제거 (5문항 고정 — 비교 일관성)
- ❌
temperature,seed변경 (0 / 42 고정) - ❌ opencode 의 기존 vllm-36b 항목 수정 (Step B 시 신규 항목 추가만)
- ❌ C1 데이터(
sft_data.jsonl)·골든셋(eval/golden.jsonl) 변경
7. 트러블슈팅
- scaffold 출력이 max_tokens 에서 잘림 →
max_tokens=1200으로 재호출 후 5라벨 재검사 - 8B :8002 응답 없음 →
docker logs vllm_eval | tail확인. 컨테이너 죽었으면 ouroboros: 다음 명령으로 재기동:docker rm -f vllm_eval 2>/dev/null docker run -d --name vllm_eval --gpus all --network host --ipc host \ --ulimit memlock=-1 --ulimit stack=67108864 \ -v /home/windpacer/.cache/huggingface:/root/.cache/huggingface \ -v /home/windpacer/.cache/vllm:/root/.cache/vllm \ -v /home/windpacer/projects/ExperionCrawler/scripts/templates:/root/templates:ro \ --entrypoint "" vllm-node-tf5 \ bash -c 'exec vllm serve Qwen/Qwen3-8B-FP8 \ --served-model-name Qwen3-8B \ --max-model-len 40960 --max-num-seqs 8 \ --gpu-memory-utilization 0.20 \ --port 8002 --host 0.0.0.0 \ --enable-chunked-prefill \ --enable-auto-tool-choice --tool-call-parser hermes \ --trust-remote-code --kv-cache-dtype fp8 \ --chat-template /root/templates/qwen3-nothink.jinja \ -tp 1' FAKE-param다수 발생 → 새 invention 모드. Verifier R6 후보로 별도 보고: "find_tags 의 허용 인자: query, area, sub_area, top_k 만. 그 외 인자 거부"
완료 보고 받으면 다음 단계:
- 8B 채택 시 → C2 LoRA 학습지시서 (Qwen3-8B bf16 베이스)
- 35B 채택 시 → C2 LoRA 학습지시서 (Qwen3.6-35B-A3B attention-only, MoE-safe)