+ {question}
+ Options: A: {choice_a}, B: {choice_b}, C: {choice_c}, D: {choice_d}
+
+ 주어진 선택지 중 해당 옵션의 문자로 바로 답하세요.
+ ```
+ Mistral3 채팅 템플릿(`[INST]...[/INST]`)으로 감싸면 `[/INST]` 직후 공백 없이 바로 `A`/`B`/`C`/`D` 토큰이 이어지는 것을 실측으로 확인했다 (" A"처럼 공백이 붙은 토큰은 별개 id).
+- **채점 방식**: KMMLU와 동일하게 log-likelihood 기반 4지선다. `generate()` 호출 없이 `[/INST]` 직후 위치의 logit에서 "A"/"B"/"C"/"D" (공백 없음) 토큰의 확률을 비교해 argmax 선택.
+- **구현**: `pseudo/eval_kdtcbench.py`. `AutoProcessor`로 이미지+텍스트를 함께 인코딩하여 `pixel_values`를 모델에 전달한다.
+- **양자화 모델 로딩 이슈**: 문서 이미지는 해상도가 높아 vision tower를 `eager` 어텐션으로 돌리면 O(N²) 어텐션 행렬이 12GB VRAM을 초과해 OOM이 발생한다. 반면 GPTQ 4-bit 기본 백엔드(Marlin)는 이 환경(RTX 5070, sm_120/Blackwell)에서 bf16 커널 JIT 컴파일이 실패하고, FOEM 3-bit는 `TritonV2Linear`가 3-bit를 지원하지 않는다. 최종적으로 `attn_implementation="sdpa"` + 커널 자동 선택(`auto → gptq_triton → eager` 순 폴백)으로 4-bit는 Marlin 실패 시 Triton으로, 3-bit는 자동으로 `TorchLinear`로 떨어지도록 구현했다.
+
+---
+
+## 3. 전체 결과
+
+| 모델 | 정확도 (micro=macro, n=240) | document (n=80) | table (n=80) | chart (n=80) | 소요 시간 |
+|---|---:|---:|---:|---:|---:|
+| **BF16 원본** | **62.08%** (149/240) | 71.25% (57/80) | 62.50% (50/80) | 52.50% (42/80) | 1.4분 |
+| **GPTQ 4-bit** | **60.83%** (146/240) | 67.50% (54/80) | 65.00% (52/80) | 50.00% (40/80) | 1.4분 |
+| **FOEM 3-bit** | **50.83%** (122/240) | 58.75% (47/80) | 56.25% (45/80) | 37.50% (30/80) | 1.5분 |
+| 4지선다 무작위 기준선 | 25.00% | 25.00% | 25.00% | 25.00% | - |
+
+카테고리별 문항 수가 80개씩 균등해 micro==macro accuracy로 일치한다.
+
+**양자화로 인한 정확도 저하 (BF16 대비)**
+
+| 모델 | 전체 | document | table | chart |
+|---|---:|---:|---:|---:|
+| GPTQ 4-bit | -1.25%p | -3.75%p | **+2.50%p** | -2.50%p |
+| FOEM 3-bit | -11.25%p | -12.50%p | -6.25%p | **-15.00%p** |
+
+- GPTQ 4-bit의 전체 손실(-1.25%p)은 KMMLU(-2.69%p)보다 오히려 작다 — table 카테고리에서는 BF16보다 **더 높은** 정확도(+2.50%p)를 기록했다. vision tower가 BF16로 그대로 유지되고 텍스트 디코더만 4-bit로 양자화되므로, "이미지에서 정확한 시각 정보를 뽑아내는" 능력 자체는 거의 보존되는 것으로 보인다.
+- FOEM 3-bit는 KMMLU와 마찬가지로 큰 폭의 손실(-11.25%p)을 보이며, 특히 **chart 카테고리에서 -15.00%p로 손실이 가장 크다**. 차트는 막대·선 그래프의 상대적 크기·순서를 비교해야 하는 다단계 추론이 필요한데, 3-bit 양자화로 인한 텍스트 추론 능력 저하가 이 다단계 비교에서 특히 두드러지는 것으로 해석된다.
+
+---
+
+## 4. 카테고리별 분석
+
+### 4-1. document (문서)
+
+| 모델 | 정확도 |
+|---|---:|
+| BF16 원본 | 71.25% |
+| GPTQ 4-bit | 67.50% |
+| FOEM 3-bit | 58.75% |
+
+세 모델 모두 3개 카테고리 중 가장 정확도가 높다 — 문서는 표·차트에 비해 텍스트 비중이 높아 OCR 유사 판독 후 직접적인 텍스트 매칭으로 풀리는 문항이 많기 때문으로 보인다.
+
+### 4-2. table (표)
+
+| 모델 | 정확도 |
+|---|---:|
+| BF16 원본 | 62.50% |
+| GPTQ 4-bit | **65.00%** |
+| FOEM 3-bit | 56.25% |
+
+유일하게 GPTQ 4-bit가 BF16을 앞서는 카테고리다 (+2.50%p, n=80이므로 2문항 차이 — 통계적 노이즈일 가능성과 실제 개선 가능성이 병존한다). 표는 행·열 대응이 명확해 특정 셀 값만 정확히 읽으면 되는 경우가 많아, 텍스트 디코더의 양자화 손실이 상대적으로 덜 치명적이었을 수 있다.
+
+### 4-3. chart (차트)
+
+| 모델 | 정확도 |
+|---|---:|
+| BF16 원본 | 52.50% |
+| GPTQ 4-bit | 50.00% |
+| FOEM 3-bit | **37.50%** |
+
+세 모델 모두 최저 정확도이며, FOEM 3-bit의 손실도 최대(-15.00%p)다. 차트는 막대 높이·선 기울기 비교, 범례-데이터 매핑 등 시각 정보를 종합해 추론해야 하는 문항이 많아 3B 규모 모델의 근본적 한계와 양자화 손실이 함께 작용하는 것으로 보인다.
+
+### 4-4. 모델 간 정답/오답 일치도 (n=240)
+
+| 패턴 | 문항 수 | 비율 |
+|---|---:|---:|
+| 세 모델 모두 정답 | 101 | 42.1% |
+| 세 모델 모두 오답 | 67 | 27.9% |
+| BF16+GPTQ 정답, FOEM만 오답 | 31 | 12.9% |
+| BF16만 정답 | 10 | 4.2% |
+| GPTQ만 정답 | 10 | 4.2% |
+| FOEM만 정답 | 10 | 4.2% |
+| BF16+FOEM 정답, GPTQ만 오답 | 7 | 2.9% |
+| GPTQ+FOEM 정답, BF16만 오답 | 4 | 1.7% |
+
+- BF16이 GPTQ보다 나은 문항 17개 vs GPTQ가 BF16보다 나은 문항 14개 → 순 우위 +3문항으로 거의 대등하다.
+- BF16이 FOEM보다 나은 문항 41개 vs FOEM이 BF16보다 나은 문항 14개 → 순 우위 +27문항으로, FOEM 3-bit의 손실이 명확하다.
+- "세 모델 모두 오답"이 27.9%에 달해, 상당수 문항은 양자화와 무관하게 **3B 모델 자체의 시각 추론 한계**임을 시사한다 (KMMLU의 Math 과목과 유사한 패턴).
+
+---
+
+## 5. 실제 질의 / 추론 예시
+
+동일한 zero-shot 프롬프트와 이미지로 세 모델을 질의하고 "A"~"D" 토큰 확률분포를 비교했다.
+
+### 5-1. document_0 (세 모델 모두 정답, 확신도 역전)
+
+> **질문**: 보고서의 주요 내용이 아닌 것은 무엇인가요?
+> - A. 안전 인프라 확충 B. 재난 및 사고 예방 체계 구축 C. 시민 안전 교육 강화 D. 긴급 대응 시스템 개선
+>
+> **정답: B**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **B** | 44.8% | **50.8%** | 2.9% | 1.5% | ✓ |
+| GPTQ 4-bit | **B** | 20.5% | **71.5%** | 6.6% | 1.4% | ✓ |
+| FOEM 3-bit | **B** | 26.9% | **64.5%** | 5.6% | 3.0% | ✓ |
+
+세 모델 모두 정답을 맞혔지만, KMMLU 보고서의 일반적 패턴(BF16이 가장 확신)과 달리 여기서는 **GPTQ 4-bit(71.5%)가 BF16(50.8%)보다 더 확신에 찬 정답**을 냈다. BF16은 오답 A에도 44.8%를 배분해 두 선택지 사이에서 흔들렸다.
+
+### 5-2. document_11 (BF16만 정답 — 세부 정보 판독 실패)
+
+> **질문**: 해당 문서의 내용과 비교하였을때, 옳지 않은 선택지는 무엇인가요?
+> - A. 신년맞이 콘서트는 2023년 12월 31일 오후 8시에 개최된다.
+> - B. 콘서트의 개최 장소는 서울 올림픽 공원 체조경기장이다.
+> - C. 티켓의 QR코드가 첨부되어 있다.
+> - D. 좌석은 VIP석 B열 15번이다.
+>
+> **정답: D**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **D** | 0.1% | 2.6% | 0.8% | **96.6%** | ✓ |
+| GPTQ 4-bit | **C** | 0.7% | 6.9% | **65.2%** | 27.2% | ✗ |
+| FOEM 3-bit | **C** | 2.7% | 4.0% | **58.1%** | 35.2% | ✗ |
+
+BF16은 96.6%의 확신으로 정답(D, 좌석 정보 불일치)을 맞혔지만, 두 양자화 모델은 모두 C(QR코드 유무)로 예측이 뒤바뀐다. 이미지 내 좌석 번호처럼 **작은 글씨의 세부 텍스트를 정밀하게 읽어야 하는 문항에서 텍스트 디코더 양자화가 시각 특징 활용의 정밀도를 떨어뜨리는** 사례로 보인다.
+
+### 5-3. table_43 (BF16만 정답 — 표 안 숫자값 오독)
+
+> **질문**: 2023년 시내버스 요금은 얼마입니까?
+> - A. 850원 B. 1000원 C. 1200원 D. 1300원
+>
+> **정답: D**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **D** | 0.0% | 0.1% | 18.2% | **81.7%** | ✓ |
+| GPTQ 4-bit | **C** | 0.0% | 7.0% | **85.1%** | 7.9% | ✗ |
+| FOEM 3-bit | **C** | 0.7% | 7.1% | **49.0%** | 43.2% | ✗ |
+
+document_11과 동일한 구조 — BF16은 정답에 81.7%로 확신했지만, GPTQ는 오히려 오답 C에 85.1%라는 더 높은 확신을 보인다. 표에서 인접한 숫자값(1200원 vs 1300원)을 구분하는 정밀도가 4-bit 양자화로 손상된 사례다.
+
+### 5-4. chart_27 (BF16만 정답 — 양자화가 확신에 찬 오답을 유발)
+
+> **질문**: 15-24세 연령대에서 가장 많이 사용하는 SNS 플랫폼은 무엇인가요?
+> - A. 페이스북 B. 인스타그램 C. 트위터 D. 틱톡
+>
+> **정답: B**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **B** | 1.1% | **98.2%** | 0.5% | 0.2% | ✓ |
+| GPTQ 4-bit | **A** | **67.7%** | 32.0% | 0.1% | 0.1% | ✗ |
+| FOEM 3-bit | **A** | **87.7%** | 9.8% | 1.3% | 1.2% | ✗ |
+
+BF16은 98.2%라는 거의 확정적인 확신으로 정답을 맞혔지만, GPTQ·FOEM은 모두 A로 예측이 뒤집히며 비트 수가 낮을수록(FOEM 87.7% > GPTQ 67.7%) 오답에 대한 확신도 더 커진다. 차트 막대의 상대적 높이를 비교하는 시각 판독이 텍스트 디코더 양자화만으로도 완전히 다른 결론으로 이어질 수 있음을 보여준다.
+
+### 5-5. document_20 (BF16만 정답 — 양자화 모델의 극단적 과확신)
+
+> **질문**: 보고서에서 하천 오염도를 어떻게 예상하고 있나요?
+> - A. 5% 상승 B. 5% 하락 C. 10% 상승 D. 10% 하락
+>
+> **정답: A**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **A** | **85.3%** | 3.1% | 11.5% | 0.0% | ✓ |
+| GPTQ 4-bit | **C** | 5.5% | 2.3% | **92.1%** | 0.0% | ✗ |
+| FOEM 3-bit | **C** | 3.3% | 7.5% | **80.2%** | 9.0% | ✗ |
+
+BF16(85.3%)과 GPTQ(92.1%로 오답 C)가 서로 정반대 방향으로 강하게 확신하는 사례다. 수치의 크기(5% vs 10%)만 다른 선택지를 구분하는 문항으로, document_11·table_43과 함께 **"근소하게 다른 숫자/텍스트를 정밀 판독해야 하는 문항"에서 텍스트 디코더 양자화가 오히려 확신도를 오답 쪽으로 증폭시키는 공통 패턴**을 보여준다.
+
+### 5-6. document_41 (FOEM만 정답 — 양자화가 우연히 도움된 사례)
+
+> **질문**: 회사의 1년 총 순이익은 얼마인가요?
+> - A. 800억원 B. 900억원 C. 1000억원 D. 1100억원
+>
+> **정답: B**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **C** | 0.9% | 29.3% | **45.4%** | 24.3% | ✗ |
+| GPTQ 4-bit | **C** | 1.0% | 34.3% | **44.0%** | 20.8% | ✗ |
+| FOEM 3-bit | **B** | 1.4% | **53.1%** | 15.2% | 30.3% | ✓ |
+
+BF16과 GPTQ는 모두 인접값 C(1000억원)로 오답이지만, FOEM 3-bit만 정답(B, 900억원)을 맞혔다. KMMLU 보고서의 Education 예시(5-5)와 같은 구조 — **문항 전체 통계로는 FOEM이 가장 낮은 정확도(document 58.75%)지만, 개별 문항에서는 저비트 양자화가 우연히 더 나은 예측을 내는 경우가 존재**한다.
+
+### 5-7. table_12 (FOEM만 정답 — 확률 분산 패턴 역전)
+
+> **질문**: 주거지역의 행정구역에 포함되지 않는 것은?
+> - A. 학교 B. 상가 C. 공원 D. 아파트단지
+>
+> **정답: B**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **C** | 1.6% | 13.0% | **84.6%** | 0.8% | ✗ |
+| GPTQ 4-bit | **C** | 3.6% | 38.3% | **55.7%** | 2.4% | ✗ |
+| FOEM 3-bit | **B** | 24.1% | **57.7%** | 11.4% | 6.9% | ✓ |
+
+BF16 → GPTQ → FOEM 순으로 오답 C에 대한 확신이 84.6% → 55.7% → 11.4%로 점차 낮아지고, 정답 B에 대한 확신은 13.0% → 38.3% → 57.7%로 점차 높아지는 **선형적 역전 패턴**이 뚜렷하다. document_41과 마찬가지로 텍스트 카테고리 판단(행정구역 vs 시설물 분류)이 관련된 문항으로, 시각 판독보다 언어적 상식 추론에 가까운 문항에서는 저비트 양자화가 오히려 유리하게 작용하기도 한다는 점을 시사한다 (단, n=1 사례이므로 일반화에는 주의가 필요하다).
+
+---
+
+## 6. 결론
+
+1. **BF16 원본(62.08%)이 기준선**이며, GPTQ 4-bit는 -1.25%p, FOEM 3-bit는 -11.25%p 손실을 보인다. KMMLU(-2.69%p / -11.30%p)와 비교하면 **GPTQ 4-bit의 K-DTCBench 손실이 KMMLU보다 오히려 작다** — vision tower가 BF16로 유지되고 텍스트 디코더만 4-bit로 양자화되므로, 시각 정보 추출 자체는 잘 보존되는 것으로 보인다. FOEM 3-bit는 두 벤치마크 모두에서 비슷한 폭(-11%p대)의 손실을 보여, 3-bit 양자화의 손실은 태스크 유형과 무관하게 일관되게 크다.
+2. **카테고리별로는 chart가 가장 취약**하다 (BF16 52.50% — 3개 중 최저, FOEM 손실도 -15.00%p로 최대). 다단계 시각 비교·추론이 필요한 차트 문항이 3B 규모 모델과 저비트 양자화의 이중 한계에 가장 취약함을 시사한다.
+3. **table 카테고리에서는 GPTQ 4-bit가 BF16을 앞선다** (+2.50%p, n=80 기준 2문항 차이). 표는 셀 단위로 값이 명확히 분리되어 있어 양자화 손실의 영향이 상대적으로 작을 수 있으나, 표본 크기가 작아 노이즈일 가능성도 배제할 수 없다.
+4. **"근소하게 다른 숫자/텍스트를 정밀 판독해야 하는 문항"(5-2, 5-3, 5-5)에서 텍스트 디코더 양자화가 확신도를 오답 쪽으로 크게 증폭시키는 공통 패턴**이 관찰된다 — BF16이 90%대로 확신하던 정답을, 양자화 모델이 80~90%대로 확신하며 정반대 오답을 고르는 사례가 반복된다.
+5. **세 모델 모두 오답인 문항이 27.9%**에 달해, K-DTCBench의 상당 부분은 양자화와 무관한 **3B 모델 자체의 시각 추론 한계**임을 보여준다 (KMMLU의 Math 과목과 유사).
+6. **후속 실험 제안**: ① 디지털 vs 수기(handwritten) 이미지 하위 그룹 분리 평가 (현재 로드된 데이터셋 컬럼에는 해당 구분이 노출되어 있지 않아 추가 메타데이터 확인 필요); ② 동일 비트 FOEM vs GPTQ 비교; ③ chart 카테고리 전용 chain-of-thought 프롬프트 실험.
+
+---
+
+## 7. 산출물
+
+- `pseudo/eval_kdtcbench.py` — 독립 실행형 K-DTCBench 평가 스크립트 (BF16 원본 및 양자화 모델 모두 지원).
+- `pseudo/collect_kdtcbench_predictions.py` — 3-way 확률분포 수집 스크립트, `logs/kdtcbench_predictions.json` 산출.
+- `logs/base_kdtcbench.log`, `logs/gptq_kdtcbench.log`, `logs/foem_kdtcbench.log` — 모델별 평가 원본 로그.
+- `logs/kdtcbench_predictions.json` — 240문항 × 3모델 전체 예측/확률분포 (보고서 5절 예시 선정에 사용).
+- `Ministral-3-3B-Instruct-2512-BF16_gptq_4bit/README.md` — `## K-DTCBench 평가 결과` 섹션 포함.
+- `Ministral-3-3B-Instruct-2512-BF16_foem_3bit/README.md` — 동일.
+- `pseudo/KDTCBENCH_REPORT.md` — 이 파일.
diff --git a/FOEM/pseudo/KMMLU_REPORT.md b/FOEM/pseudo/KMMLU_REPORT.md
new file mode 100644
index 0000000..2824b05
--- /dev/null
+++ b/FOEM/pseudo/KMMLU_REPORT.md
@@ -0,0 +1,279 @@
+# Ministral-3-3B KMMLU 평가 비교 보고서
+**BF16 원본 vs GPTQ 4-bit vs FOEM 3-bit (5-shot, KMMLU 45개 과목 전체)**
+
+작성일: 2026-07-01 · 모델: `mistralai/Ministral-3-3B-Instruct-2512-BF16`
+
+---
+
+## 1. 목적
+
+WikiText-2 perplexity와 KMMLU 정확도를 세 가지 모델 변형에 걸쳐 비교한다: ① **BF16 원본** (비양자화, 기준선), ② **GPTQ 4-bit**, ③ **FOEM 3-bit** (α=0, β=0.2). 이를 통해 각 양자화 방법이 한국어 도메인 지식·추론 능력을 얼마나 보존/손실시키는지 정량화한다.
+
+| 모델 | 양자화 방법 | 비트 | WikiText-2 PPL |
+|---|---|---|---|
+| `Ministral-3-3B-Instruct-2512-BF16` | 없음 (기준) | BF16 | N/A |
+| `Ministral-3-3B-Instruct-2512-BF16_gptq_4bit` | GPTQ | 4-bit | 8.72 |
+| `Ministral-3-3B-Instruct-2512-BF16_foem_3bit` | FOEM (α=0, β=0.2) | 3-bit | 10.87 |
+
+---
+
+## 2. 평가 방법
+
+- **데이터셋**: `HAERAE-HUB/KMMLU` — 단일 "All" config 없음, 45개 과목별 config 순회 누적.
+- **Few-shot**: 5-shot (각 과목 `dev` split 5문항, KMMLU 공식 벤치마크와 동일 설정).
+- **채점 방식**: log-likelihood 기반 4지선다 (generate() 없음). 5-shot 프롬프트 뒤에서 " A"/" B"/" C"/" D" 토큰의 logit argmax.
+- **구현**: `pseudo/eval_kmmlu.py`. BF16 원본은 `Mistral3ForConditionalGeneration.from_pretrained()` + `tie_weights()` 호출, 양자화 모델은 `GPTQModel.load()` + `.model` 추출 패턴.
+
+---
+
+## 3. 전체 결과
+
+| 모델 | 정확도 (micro) | 정확도 (macro) | 소요 시간 | 문항 수 |
+|---|---:|---:|---:|---:|
+| **BF16 원본** | **46.88%** | 46.02% | 47.1분 | 35,030 |
+| **GPTQ 4-bit** | **44.19%** | 43.42% | 62.4분 | 35,030 |
+| **FOEM 3-bit** | **35.58%** | 35.03% | 64.9분 | 35,030 |
+| 4지선다 무작위 기준선 | 25.00% | 25.00% | - | - |
+
+**양자화로 인한 정확도 저하 (BF16 대비)**
+
+| 모델 | micro 차이 | macro 차이 |
+|---|---:|---:|
+| GPTQ 4-bit | -2.69%p | -2.60%p |
+| FOEM 3-bit | -11.30%p | -10.99%p |
+
+---
+
+## 4. 과목별 분석
+
+45개 과목 전체 3-way 비교 표
+
+| 과목 | BF16 | GPTQ 4-bit | FOEM 3-bit | 문항 수 |
+|---|---:|---:|---:|---:|
+| Accounting | 41.0% | 37.0% | 37.0% | 100 |
+| Agricultural-Sciences | 37.7% | 36.1% | 28.0% | 1000 |
+| Aviation-Engineering-and-Maintenance | 44.7% | 41.5% | 35.3% | 1000 |
+| Biology | 38.9% | 36.9% | 28.8% | 1000 |
+| Chemical-Engineering | 49.8% | 47.3% | 37.0% | 1000 |
+| Chemistry | 51.8% | 48.0% | 36.5% | 600 |
+| Civil-Engineering | 44.2% | 41.5% | 31.3% | 1000 |
+| Computer-Science | 72.0% | 69.2% | 54.1% | 1000 |
+| Construction | 36.8% | 34.4% | 32.3% | 1000 |
+| Criminal-Law | 34.0% | 33.5% | 27.5% | 200 |
+| Ecology | 48.7% | 45.8% | 31.4% | 1000 |
+| Economics | 46.9% | 46.9% | 39.2% | 130 |
+| Education | 64.0% | 58.0% | 37.0% | 100 |
+| Electrical-Engineering | 35.3% | 34.3% | 26.4% | 1000 |
+| Electronics-Engineering | 54.5% | 52.9% | 37.9% | 1000 |
+| Energy-Management | 34.0% | 33.1% | 27.5% | 1000 |
+| Environmental-Science | 35.0% | 30.4% | 25.2% | 1000 |
+| Fashion | 46.4% | 44.6% | 35.1% | 1000 |
+| Food-Processing | 43.6% | 41.9% | 34.4% | 1000 |
+| Gas-Technology-and-Engineering | 39.9% | 36.4% | 30.3% | 1000 |
+| Geomatics | 44.5% | 39.8% | 32.6% | 1000 |
+| Health | 64.0% | 55.0% | 37.0% | 100 |
+| Industrial-Engineer | 44.3% | 40.5% | 33.0% | 1000 |
+| Information-Technology | 69.1% | 64.4% | 49.1% | 1000 |
+| Interior-Architecture-and-Design | 53.2% | 50.8% | 41.2% | 1000 |
+| Law | 45.7% | 42.6% | 33.3% | 1000 |
+| Machine-Design-and-Manufacturing | 44.4% | 42.8% | 34.4% | 1000 |
+| Management | 53.9% | 49.0% | 41.6% | 1000 |
+| Maritime-Engineering | 47.0% | 44.8% | 35.2% | 600 |
+| Marketing | 75.4% | 74.9% | 59.9% | 1000 |
+| Materials-Engineering | 48.9% | 44.8% | 35.5% | 1000 |
+| Mechanical-Engineering | 43.0% | 39.4% | 32.1% | 1000 |
+| Nondestructive-Testing | 47.4% | 45.4% | 36.6% | 1000 |
+| Patent | 28.0% | 32.0% | 25.0% | 100 |
+| Political-Science-and-Sociology | 52.0% | 47.3% | 38.0% | 300 |
+| Psychology | 45.5% | 43.2% | 34.6% | 1000 |
+| Public-Safety | 39.8% | 38.1% | 28.9% | 1000 |
+| Railway-and-Automotive-Engineering | 40.5% | 38.6% | 31.9% | 1000 |
+| Real-Estate | 38.5% | 38.5% | 36.5% | 200 |
+| Refrigerating-Machinery | 37.2% | 32.7% | 28.2% | 1000 |
+| Social-Welfare | 55.8% | 52.6% | 38.6% | 1000 |
+| Taxation | 38.0% | 40.0% | 32.5% | 200 |
+| Telecommunications-and-Wireless-Technology | 59.0% | 55.5% | 41.3% | 1000 |
+| Korean-History | 33.0% | 27.0% | 33.0% | 100 |
+| Math | 23.7% | 24.3% | 23.7% | 300 |
+
+
+
+### BF16 대비 GPTQ가 크게 저하된 과목 (top 5 손실)
+
+| 과목 | BF16 | GPTQ | 차이 | 문항 수 |
+|---|---:|---:|---:|---:|
+| Health | 64.0% | 55.0% | -9.0%p | 100 |
+| Education | 64.0% | 58.0% | -6.0%p | 100 |
+| Korean-History | 33.0% | 27.0% | -6.0%p | 100 |
+| Management | 53.9% | 49.0% | -4.9%p | 1000 |
+| Geomatics | 44.5% | 39.8% | -4.7%p | 1000 |
+
+### BF16 대비 FOEM이 크게 저하된 과목 (top 5 손실)
+
+| 과목 | BF16 | FOEM | 차이 | 문항 수 |
+|---|---:|---:|---:|---:|
+| Education | 64.0% | 37.0% | -27.0%p | 100 |
+| Health | 64.0% | 37.0% | -27.0%p | 100 |
+| Information-Technology | 69.1% | 49.1% | -20.0%p | 1000 |
+| Computer-Science | 72.0% | 54.1% | -17.9%p | 1000 |
+| Telecommunications-and-Wireless-Technology | 59.0% | 41.3% | -17.7%p | 1000 |
+
+- BF16이 GPTQ를 앞서는 과목: **40/45개**, BF16이 FOEM을 앞서는 과목: **43/45개**.
+
+---
+
+## 5. 실제 질의 / 추론 예시
+
+동일한 5-shot 프롬프트로 세 모델에 같은 문항(각 과목 test[0])을 질의하고, " A"~" D" 중 어디에 가장 높은 확률을 두었는지 비교했다.
+
+### 5-1. Korean-History (세 모델 모두 오답, 정답 C에 대한 확신도 비교)
+
+> **질문**: 밑줄 친 '왕'의 재위 기간에 있었던 사실로 옳은 것은? 왕은 노론과 소론, 남인을 두루 등용하였으며 젊은 관료들을 재교육하기 위해 초계문신제를 시행하였다. 또 서얼 출신의 유능한 인사를 규장각 검서관으로 등용하였다.
+> - A. 동학이 창시되었다.
+> - B. 대전회통이 편찬되었다.
+> - C. 신해통공이 시행되었다.
+> - D. 홍경래의 난이 발생하였다.
+>
+> **정답: C** (정조 시대 — 신해통공 시행)
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **B** | 9.8% | **49.5%** | 14.2% | 26.5% | ✗ |
+| GPTQ 4-bit | **B** | 18.8% | **45.2%** | 14.7% | 21.3% | ✗ |
+| FOEM 3-bit | **B** | 17.9% | 29.5% | 23.0% | 29.5% | ✗ |
+
+세 모델 모두 정조(규장각·초계문신제)를 영조·순조 시기 정책과 혼동해 오답(B, "대전회통" — 고종 대 편찬)을 골랐다. BF16이 B에 49.5%로 가장 높은 확신을 보인 반면, FOEM은 B와 D에 확률이 분산되어 정답 C에 상대적으로 높은 23.0%를 배분했다. **3B 모델 자체가 이 문항에서 틀리는 것이며, 양자화로 오답이 생긴 게 아님을 확인할 수 있다.**
+
+### 5-2. Math (세 모델 모두 오답, 무작위에 가까운 분포)
+
+> **질문**: 함수 f(x)=-x³+2x+3에 대하여 직선 y=-x+k와 곡선 y=f(x)의 그래프가 서로 다른 두 점에서 만나도록 하는 모든 양수 k의 합은?
+> - A. 6 B. 12 C. 18 D. 36
+>
+> **정답: A**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **D** | 9.4% | 28.9% | 28.9% | **32.8%** | ✗ |
+| GPTQ 4-bit | **B** | 12.6% | **34.2%** | 26.6% | 26.6% | ✗ |
+| FOEM 3-bit | **D** | 18.6% | 27.0% | 23.8% | **30.6%** | ✗ |
+
+세 모델 모두 정답(A)에 낮은 확률을 부여하고 B·C·D에 고르게 분산돼 있다 — 베이스 모델(BF16)도 동일하게 틀리므로, 이는 **양자화 손실이 아닌 3B 규모 모델의 수리 추론 한계**에 기인한다.
+
+### 5-3. Computer-Science (세 모델 모두 정답, BF16이 가장 확신)
+
+> **질문**: NTFS 파일시스템에서 부팅과정에서 읽어들이는 부분으로 디스크에 저장되어 있는 파일의 정보DB를 담아 놓은 것은?
+> - A. VFAT B. MFT C. PROM D. CMOS
+>
+> **정답: B**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **B** | 0.1% | **99.2%** | 0.4% | 0.3% | ✓ |
+| GPTQ 4-bit | **B** | 0.2% | **97.9%** | 1.4% | 0.6% | ✓ |
+| FOEM 3-bit | **B** | 14.5% | **69.3%** | 8.8% | 7.3% | ✓ |
+
+사실 기반(factoid) 문항에서는 세 모델 모두 정답을 맞혔다. BF16(99.2%) > GPTQ(97.9%) > FOEM(69.3%) 순으로 확신도가 낮아지는 패턴이 명확하며, 양자화 수준이 낮을수록(비트 수 감소) 분포가 넓어지는(less sharp) 경향을 보인다.
+
+### 5-4. Marketing (세 모델 모두 오답, 양자화 심화될수록 과확신 심화)
+
+> **질문**: 표본을 통해서 모집단의 성질을 정확히 추론하기 위하여 고려할 사항으로 중요하지 않은 것은?
+> - A. 모집단 요소들의 동질성 정도 B. 표본의 크기 C. 표본선정 시기 D. 표본조사 예산
+>
+> **정답: C**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **D** | 1.3% | 1.0% | 36.9% | **60.8%** | ✗ |
+| GPTQ 4-bit | **D** | 1.6% | 1.8% | 28.4% | **68.2%** | ✗ |
+| FOEM 3-bit | **D** | 3.7% | 1.3% | 4.8% | **90.2%** | ✗ |
+
+세 모델 모두 오답(D)을 골랐지만, BF16은 정답(C)에 36.9%를 분산해 두었고 GPTQ는 28.4%, FOEM은 4.8%에 불과하다. 즉 **BF16 → GPTQ → FOEM 순서로 오답에 대한 과확신(overconfidence)이 심화**되는 패턴이 뚜렷하다. FOEM 3-bit의 경우 D에 90.2%를 몰아 정답을 사실상 배제했다 — 비트 감소에 따른 확률 분포 왜곡이 누적되는 사례다.
+
+### 5-5. Education (FOEM만 정답 — 양자화가 오히려 도움이 된 사례)
+
+> **질문**: 항존주의 교육철학에 대한 설명으로 옳은 것은?
+> - A. 아동 존중의 원리를 채택한다.
+> - B. 교육을 통한 사회 개조를 중시한다.
+> - C. 지식이나 진리의 영원성을 강조한다.
+> - D. 실제적인 삶의 문제를 해결하는 데 초점을 둔다.
+>
+> **정답: C** (항존주의 — 불변하는 지식·진리 강조)
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **D** | 13.0% | 2.3% | 18.9% | **65.9%** | ✗ |
+| GPTQ 4-bit | **D** | 19.7% | 5.3% | 6.0% | **68.9%** | ✗ |
+| FOEM 3-bit | **C** | 13.4% | 7.2% | **60.0%** | 19.5% | ✓ |
+
+BF16과 GPTQ는 모두 D를 골라 오답이지만, FOEM 3-bit는 C를 60.0%의 확신으로 정답을 맞혔다. GPTQ는 오히려 C 확률을 18.9% → 6.0%로 낮춰 정답에서 더 멀어졌다. 이처럼 **특정 문항에서 낮은 비트 양자화가 우연히 더 나은 예측을 내는 사례**가 존재한다 — 단, 과목 전체 정확도는 BF16(64%) > GPTQ(58%) > FOEM(37%)이므로 이 문항은 FOEM에게 운 좋은 예외에 해당한다.
+
+### 5-6. Patent (세 모델 모두 정답, 확신도 순차 감소)
+
+> **질문**: 의사표시의 효력발생에 관한 설명으로 옳지 않은 것은?
+> - A. 준법률행위의 도달은 … 객관적 상태에 놓여졌을 때를 말한다.
+> - B. 의사표시의 상대방이 정당한 사유 없이 통지의 수령을 거절한 경우 … 효력이 발생한다.
+> - C. 채권양도의 통지는 채무자의 주소 등에 해당하지 아니하는 장소에서라도 … 효력이 발생한다.
+> - D. 보통우편의 방법으로 의사표시를 통지한 경우에도 발송되었다는 사실만 증명되면, 상당한 기간 내에 도달한 것으로 추정된다.
+>
+> **정답: D**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **D** | 6.1% | 4.8% | 4.2% | **84.8%** | ✓ |
+| GPTQ 4-bit | **D** | 11.2% | 14.4% | 9.9% | **64.5%** | ✓ |
+| FOEM 3-bit | **D** | 27.8% | 11.6% | 14.9% | **45.8%** | ✓ |
+
+세 모델 모두 정답(D)을 골랐으나, 정답 확률이 BF16(84.8%) → GPTQ(64.5%) → FOEM(45.8%)로 비트 감소에 따라 선형적으로 낮아지는 패턴이 뚜렷하다. Computer-Science 예시(5-3)와 동일한 구조로, **사실·법령 기반 문항에서는 비트 수가 낮아질수록 확신도가 감소하지만 정답 선택 자체는 유지**된다.
+
+### 5-7. Law (세 모델 모두 오답, 각자 다른 선택지 선택)
+
+> **질문**: 경비업법령상 경비원의 명부와 배치 등에 관한 설명으로 옳은 것은?
+> - A. 경비업자는 주된 사무소, 출장소, 집단민원현장에 경비원의 명부를 작성·비치하여 두고 이를 항상 정리해야 한다.
+> - B. 경비업자는 경비원을 배치하여 … 근무상황기록부를 작성하여 2년 동안 보관해야 한다.
+> - C. 경비업자는 형법상 상해죄 또는 폭행죄를 범하여 벌금형을 선고받고 7년이 지나지 아니한 자를 집단민원현장에 일반경비원으로 배치하여서는 아니 된다.
+> - D. 관할 경찰관서장은 경비원이 위력이나 흉기 … 집단적 폭력사태를 일으킨 때에는 경비업의 허가를 취소해야 한다.
+>
+> **정답: C**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **A** | **66.1%** | 13.0% | 16.7% | 4.2% | ✗ |
+| GPTQ 4-bit | **A** | **37.4%** | 20.0% | 33.0% | 9.5% | ✗ |
+| FOEM 3-bit | **D** | 22.6% | 8.3% | 21.2% | **47.8%** | ✗ |
+
+세 모델이 각기 다른 오답을 골라, 세부 법령 지식이 전반적으로 취약하다는 것을 보여준다. BF16은 A에 66.1%로 매우 과확신하지만, GPTQ는 A(37.4%)와 C(33.0%)가 거의 균등해 오히려 정답에 더 가까운 분포를 보인다 — **이 문항에서는 GPTQ가 BF16보다 덜 확신에 찬 오답**을 낸 사례다. FOEM은 D로 완전히 다른 방향으로 이탈한다.
+
+### 5-8. Information-Technology (FOEM만 오답, 정답 확률 순위 역전)
+
+> **질문**: 일반적인 로더(general loader)에 가장 가까운 것은?
+> - A. Direct Loader B. Absolute Loader C. Compile And Go Loader D. Direct Linking Loader
+>
+> **정답: D**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| BF16 원본 | **D** | 24.6% | 14.9% | 24.6% | **35.8%** | ✓ |
+| GPTQ 4-bit | **D** | 27.8% | 19.1% | 21.6% | **31.5%** | ✓ |
+| FOEM 3-bit | **C** | 25.4% | 22.4% | **36.9%** | 15.4% | ✗ |
+
+BF16과 GPTQ는 낮은 확신(35.8%, 31.5%)이지만 정답(D)을 유지한 반면, FOEM은 D 확률이 15.4%로 급락하며 C(36.9%)로 예측이 역전됐다. 3-bit 양자화로 인해 **가중치 정밀도 손실이 확률 순위 자체를 바꿔버린** 사례로, 단순히 확신도가 줄어드는 게 아니라 예측 방향이 바뀔 수 있음을 보여준다.
+
+---
+
+## 6. 결론
+
+1. **BF16 원본(46.88%)이 기준선**이며, GPTQ 4-bit는 -2.69%p, FOEM 3-bit는 -11.30%p 차이를 보인다. 비트 수가 더 낮은 FOEM 3-bit의 손실이 더 크며, 이는 PPL 결과(8.72 vs 10.87)와 같은 방향이다.
+2. **GPTQ vs FOEM 알고리즘 비교**: GPTQ 4-bit가 FOEM 3-bit보다 micro +8.61%p 높지만, 이는 알고리즘 차이가 아닌 **비트 수 차이(4 vs 3)**가 주된 원인일 가능성이 높다. 공정한 비교를 위해 동일 비트(4-bit FOEM vs 4-bit GPTQ, 3-bit FOEM vs 3-bit GPTQ) 실험이 필요하다.
+3. **Math 과목**은 세 모델 모두 무작위 기준선(25%) 수준으로, 양자화 손실보다 **3B 베이스 모델의 수리 추론 한계**가 더 크게 작용한다.
+4. **Korean-History**는 BF16(33%)·FOEM(33%)이 GPTQ(27%)를 역전하는 패턴이 실제로 확인됐다. 예시 문항(5-1)에서도 세 모델 모두 동일 오답(B)을 골라 이는 베이스 모델 자체의 한국사 지식 한계이며, GPTQ에서 특정 지식이 더 손실되는 방향으로 작동한 것으로 추정된다 (n=100으로 노이즈 가능성 병존).
+5. **후속 실험 제안**: ① 동일 비트 FOEM vs GPTQ 비교; ② Math 과목을 위한 chain-of-thought 프롬프트 실험; ③ Calibration curve (confidence vs accuracy) 분석으로 과확신(overconfident) 오답 패턴 정량화.
+
+---
+
+## 7. 산출물
+
+- `Ministral-3-3B-Instruct-2512-BF16_gptq_4bit/README.md` — `## KMMLU 평가 결과` 섹션 포함.
+- `Ministral-3-3B-Instruct-2512-BF16_foem_3bit/README.md` — 동일.
+- `pseudo/eval_kmmlu.py` — 독립 실행형 KMMLU 평가 스크립트 (BF16 원본 및 양자화 모델 모두 지원).
+- `pseudo/quantize_mistral.py` — `eval_kmmlu_quantized()` 통합.
+- `pseudo/KMMLU_REPORT.md` — 이 파일 (3-way 비교, 베이스 모델 포함).
diff --git a/FOEM/pseudo/collect_base_examples.py b/FOEM/pseudo/collect_base_examples.py
new file mode 100644
index 0000000..e846a75
--- /dev/null
+++ b/FOEM/pseudo/collect_base_examples.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env -S python -u
+"""보고서 섹션 5용: BF16 베이스 모델로 4개 예시 문항의 A/B/C/D 확률분포 수집."""
+import os
+import torch
+import torch.nn.functional as F
+from datasets import load_dataset
+from transformers import AutoTokenizer, AutoConfig
+
+BASE_PATH = "/root/.cache/huggingface/hub/models--mistralai--Ministral-3-3B-Instruct-2512-BF16/snapshots/ecc3ba8b43a45610e709327c049d24b009bfec88"
+CHOICE_LETTERS = ["A", "B", "C", "D"]
+SUBJECTS = ["Korean-History", "Math", "Computer-Science", "Marketing"]
+
+
+def load_base_model():
+ cfg = AutoConfig.from_pretrained(BASE_PATH)
+ import transformers as _t
+ cls = getattr(_t, cfg.architectures[0])
+ hf = cls.from_pretrained(
+ BASE_PATH, config=cfg, dtype=torch.bfloat16,
+ device_map="auto", attn_implementation="sdpa"
+ )
+ hf.tie_weights()
+ hf.eval()
+ return hf
+
+
+def _format_choices(ex):
+ return "\n".join(f"{l}. {ex[l]}" for l in CHOICE_LETTERS)
+
+
+def _format_q(ex, with_answer):
+ text = f"{ex['question']}\n{_format_choices(ex)}\n정답:"
+ if with_answer:
+ text += f" {CHOICE_LETTERS[ex['answer'] - 1]}"
+ return text
+
+
+def build_prefix(subject):
+ dev = load_dataset("HAERAE-HUB/KMMLU", subject, split="dev")
+ return "\n\n".join(_format_q(ex, True) for ex in dev.select(range(5))) + "\n\n"
+
+
+@torch.inference_mode()
+def predict(hf, tok, subject):
+ prefix = build_prefix(subject)
+ test = load_dataset("HAERAE-HUB/KMMLU", subject, split="test")
+ ex = test[0]
+
+ prompt = prefix + _format_q(ex, False)
+ input_ids = tok(prompt, return_tensors="pt").input_ids.to(next(hf.parameters()).device)
+ logits = hf(input_ids=input_ids).logits[0, -1, :]
+
+ choice_ids = [tok.encode(f" {l}", add_special_tokens=False)[0] for l in CHOICE_LETTERS]
+ choice_logits = torch.stack([logits[i] for i in choice_ids])
+ probs = F.softmax(choice_logits.float(), dim=0)
+ pred_idx = int(probs.argmax())
+
+ return {
+ "subject": subject,
+ "question": ex["question"],
+ "choices": {l: ex[l] for l in CHOICE_LETTERS},
+ "answer": CHOICE_LETTERS[ex["answer"] - 1],
+ "pred": CHOICE_LETTERS[pred_idx],
+ "probs": {l: float(probs[i]) * 100 for i, l in enumerate(CHOICE_LETTERS)},
+ "correct": pred_idx == ex["answer"] - 1,
+ }
+
+
+def main():
+ tok = AutoTokenizer.from_pretrained(BASE_PATH)
+ hf = load_base_model()
+
+ results = []
+ for subj in SUBJECTS:
+ r = predict(hf, tok, subj)
+ results.append(r)
+ mark = "✓" if r["correct"] else "✗"
+ print(f"[{subj}] pred={r['pred']} answer={r['answer']} {mark}")
+ for l in CHOICE_LETTERS:
+ print(f" {l}: {r['probs'][l]:.1f}%")
+
+ # 마크다운 행 출력 (보고서 붙여넣기용)
+ print("\n--- 마크다운 행 (BF16 추가분) ---")
+ for r in results:
+ mark = "✓" if r["correct"] else "✗"
+ ps = r["probs"]
+ print(f"| BF16 원본 | **{r['pred']}** | {ps['A']:.1f}% | {ps['B']:.1f}% | {ps['C']:.1f}% | {ps['D']:.1f}% | {mark} |")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/collect_extra_examples.py b/FOEM/pseudo/collect_extra_examples.py
new file mode 100644
index 0000000..2be0954
--- /dev/null
+++ b/FOEM/pseudo/collect_extra_examples.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env -S python -u
+"""보고서 섹션 5 추가 예시용: Education / Patent / Law / Information-Technology
+세 모델(BF16 / GPTQ 4-bit / FOEM 3-bit) 순서로 로드해 A~D 확률분포 수집."""
+import gc
+import os
+import torch
+import torch.nn.functional as F
+from datasets import load_dataset
+from transformers import AutoTokenizer, AutoConfig
+
+BASE_PATH = "/root/.cache/huggingface/hub/models--mistralai--Ministral-3-3B-Instruct-2512-BF16/snapshots/ecc3ba8b43a45610e709327c049d24b009bfec88"
+GPTQ_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/Ministral-3-3B-Instruct-2512-BF16_gptq_4bit"
+FOEM_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/Ministral-3-3B-Instruct-2512-BF16_foem_3bit"
+
+SUBJECTS = ["Education", "Patent", "Law", "Information-Technology"]
+LETTERS = ["A", "B", "C", "D"]
+
+
+# ── 모델 로더 ─────────────────────────────────────────────────────────────────
+
+def load_base():
+ cfg = AutoConfig.from_pretrained(BASE_PATH)
+ import transformers as _t
+ cls = getattr(_t, cfg.architectures[0])
+ hf = cls.from_pretrained(
+ BASE_PATH, config=cfg, dtype=torch.bfloat16,
+ device_map="auto", attn_implementation="sdpa"
+ )
+ hf.tie_weights()
+ hf.eval()
+ return hf
+
+def load_quant(path):
+ from gptqmodel import GPTQModel
+ try:
+ qm = GPTQModel.load(path, attn_implementation="eager")
+ except Exception:
+ qm = GPTQModel.load(path, attn_implementation="eager", backend="gptq_triton")
+ hf = getattr(qm, "model", qm)
+ hf.eval()
+ return hf, qm
+
+def unload(hf, extra=None):
+ # GPU → CPU 이동 후 삭제해야 VRAM이 즉시 반환됨
+ try:
+ hf.cpu()
+ except Exception:
+ pass
+ del hf
+ if extra is not None:
+ del extra
+ gc.collect()
+ torch.cuda.empty_cache()
+ torch.cuda.synchronize()
+
+
+# ── 추론 ─────────────────────────────────────────────────────────────────────
+
+def _fmt(ex, with_ans):
+ choices = "\n".join(f"{l}. {ex[l]}" for l in LETTERS)
+ text = f"{ex['question']}\n{choices}\n정답:"
+ if with_ans:
+ text += f" {LETTERS[ex['answer'] - 1]}"
+ return text
+
+def build_prefix(subject):
+ dev = load_dataset("HAERAE-HUB/KMMLU", subject, split="dev")
+ return "\n\n".join(_fmt(ex, True) for ex in dev.select(range(5))) + "\n\n"
+
+@torch.inference_mode()
+def predict_one(hf, tok, subject):
+ prefix = build_prefix(subject)
+ ex = load_dataset("HAERAE-HUB/KMMLU", subject, split="test")[0]
+ prompt = prefix + _fmt(ex, False)
+ dev = next(hf.parameters()).device
+ ids = tok(prompt, return_tensors="pt").input_ids.to(dev)
+ logits = hf(input_ids=ids).logits[0, -1, :]
+ cids = [tok.encode(f" {l}", add_special_tokens=False)[0] for l in LETTERS]
+ probs = F.softmax(torch.stack([logits[i] for i in cids]).float(), dim=0)
+ pred = int(probs.argmax())
+ return {
+ "subject": subject,
+ "question": ex["question"],
+ "choices": {l: ex[l] for l in LETTERS},
+ "answer": LETTERS[ex["answer"] - 1],
+ "pred": LETTERS[pred],
+ "probs": {l: float(probs[i]) * 100 for i, l in enumerate(LETTERS)},
+ "correct": pred == ex["answer"] - 1,
+ }
+
+
+# ── 메인 ─────────────────────────────────────────────────────────────────────
+
+def run_model(label, hf, tok):
+ print(f"\n=== {label} ===")
+ results = {}
+ for s in SUBJECTS:
+ r = predict_one(hf, tok, s)
+ results[s] = r
+ mark = "✓" if r["correct"] else "✗"
+ ps = r["probs"]
+ print(f" [{s}] pred={r['pred']} ans={r['answer']} {mark} "
+ f"A:{ps['A']:.1f}% B:{ps['B']:.1f}% C:{ps['C']:.1f}% D:{ps['D']:.1f}%")
+ return results
+
+
+def main():
+ tok = AutoTokenizer.from_pretrained(BASE_PATH)
+
+ # BF16
+ print("[load] BF16 베이스 모델")
+ hf = load_base()
+ base_res = run_model("BF16 원본", hf, tok)
+ unload(hf)
+
+ # GPTQ
+ print("[load] GPTQ 4-bit")
+ hf, qm = load_quant(GPTQ_PATH)
+ gptq_res = run_model("GPTQ 4-bit", hf, tok)
+ unload(hf, qm)
+
+ # FOEM
+ print("[load] FOEM 3-bit")
+ hf, qm = load_quant(FOEM_PATH)
+ foem_res = run_model("FOEM 3-bit", hf, tok)
+ unload(hf, qm)
+
+ # ── 마크다운 출력 ──────────────────────────────────────────────────────────
+ print("\n\n" + "="*60)
+ print("마크다운 섹션 (보고서 붙여넣기용)")
+ print("="*60)
+
+ labels = [("BF16 원본", base_res), ("GPTQ 4-bit", gptq_res), ("FOEM 3-bit", foem_res)]
+
+ for s in SUBJECTS:
+ b = base_res[s]
+ print(f"\n### {s}")
+ print(f"질문: {b['question']}")
+ for l in LETTERS:
+ print(f" {l}. {b['choices'][l]}")
+ print(f"정답: {b['answer']}")
+ print()
+ print("| 모델 | 예측 | A | B | C | D | 정오 |")
+ print("|---|---|---:|---:|---:|---:|---|")
+ for label, res in labels:
+ r = res[s]
+ ps = r["probs"]
+ mark = "✓" if r["correct"] else "✗"
+ # 최고확률에 ** 강조
+ max_l = max(LETTERS, key=lambda l: ps[l])
+ cells = {l: f"**{ps[l]:.1f}%**" if l == max_l else f"{ps[l]:.1f}%" for l in LETTERS}
+ print(f"| {label} | **{r['pred']}** | {cells['A']} | {cells['B']} | {cells['C']} | {cells['D']} | {mark} |")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/collect_kdtcbench_predictions.py b/FOEM/pseudo/collect_kdtcbench_predictions.py
new file mode 100644
index 0000000..c0fb48c
--- /dev/null
+++ b/FOEM/pseudo/collect_kdtcbench_predictions.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env -S python -u
+"""K-DTCBench 보고서용: 세 모델(BF16 / GPTQ 4-bit / FOEM 3-bit) 전체 240문항에 대해
+A~D 확률분포를 수집해 JSON으로 저장한다 (보고서 5절 예시 선정 및 표 작성용)."""
+import gc
+import json
+
+import torch
+import torch.nn.functional as F
+from datasets import load_dataset
+from transformers import AutoProcessor, AutoConfig
+
+BASE_PATH = "/root/.cache/huggingface/hub/models--mistralai--Ministral-3-3B-Instruct-2512-BF16/snapshots/ecc3ba8b43a45610e709327c049d24b009bfec88"
+GPTQ_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/Ministral-3-3B-Instruct-2512-BF16_gptq_4bit"
+FOEM_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/Ministral-3-3B-Instruct-2512-BF16_foem_3bit"
+OUT_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/logs/kdtcbench_predictions.json"
+
+LETTERS = ["A", "B", "C", "D"]
+CHOICE_KEYS = ["choice_a", "choice_b", "choice_c", "choice_d"]
+
+
+def load_base():
+ cfg = AutoConfig.from_pretrained(BASE_PATH)
+ import transformers as _t
+ cls = getattr(_t, cfg.architectures[0])
+ hf = cls.from_pretrained(
+ BASE_PATH, config=cfg, dtype=torch.bfloat16,
+ device_map="auto", attn_implementation="sdpa"
+ )
+ hf.tie_weights()
+ hf.eval()
+ return hf, None
+
+
+def load_quant(path):
+ from gptqmodel import GPTQModel
+ last_err = None
+ for kwargs in (
+ {"attn_implementation": "sdpa"},
+ {"attn_implementation": "sdpa", "backend": "gptq_triton"},
+ {"attn_implementation": "eager", "backend": "gptq_triton"},
+ ):
+ try:
+ qm = GPTQModel.load(path, **kwargs)
+ break
+ except Exception as e:
+ last_err = e
+ else:
+ raise last_err
+ hf = getattr(qm, "model", qm)
+ hf.eval()
+ return hf, qm
+
+
+def unload(hf, extra=None):
+ try:
+ hf.cpu()
+ except Exception:
+ pass
+ del hf
+ if extra is not None:
+ del extra
+ gc.collect()
+ torch.cuda.empty_cache()
+ torch.cuda.synchronize()
+
+
+def _build_prompt(ex: dict) -> str:
+ options = ", ".join(f"{l}: {ex[k]}" for l, k in zip(LETTERS, CHOICE_KEYS))
+ return (
+ f"{ex['question']}\nOptions: {options}\n\n"
+ "주어진 선택지 중 해당 옵션의 문자로 바로 답하세요."
+ )
+
+
+@torch.inference_mode()
+def predict_one(hf, processor, choice_ids, ex):
+ messages = [{"role": "user", "content": [
+ {"type": "image"},
+ {"type": "text", "text": _build_prompt(ex)},
+ ]}]
+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
+ inputs = processor(images=[ex["image"]], text=text, return_tensors="pt")
+ dev = next(hf.parameters()).device
+ inputs = {k: v.to(dev) for k, v in inputs.items()}
+
+ logits = hf(**inputs).logits[0, -1, :]
+ probs = F.softmax(torch.stack([logits[i] for i in choice_ids]).float(), dim=0)
+ pred = int(probs.argmax())
+ return {
+ "pred": LETTERS[pred],
+ "probs": {l: float(probs[i]) * 100 for i, l in enumerate(LETTERS)},
+ "correct": LETTERS[pred] == ex["answer"],
+ }
+
+
+def run_model(label, hf, processor, dataset):
+ choice_ids = [processor.tokenizer.encode(l, add_special_tokens=False)[0] for l in LETTERS]
+ results = []
+ for i, ex in enumerate(dataset):
+ r = predict_one(hf, processor, choice_ids, ex)
+ r.update({"index": ex["index"], "category": ex["category"], "answer": ex["answer"]})
+ results.append(r)
+ if (i + 1) % 40 == 0:
+ print(f" [{label}] {i+1}/{len(dataset)}")
+ acc = sum(r["correct"] for r in results) / len(results)
+ print(f"[{label}] accuracy = {acc:.2%}")
+ return results
+
+
+def main():
+ processor = AutoProcessor.from_pretrained(BASE_PATH)
+ dataset = load_dataset("NCSOFT/K-DTCBench", split="test")
+
+ all_results = {}
+
+ print("[load] BF16 베이스 모델")
+ hf, extra = load_base()
+ all_results["bf16"] = run_model("BF16", hf, processor, dataset)
+ unload(hf, extra)
+
+ print("[load] GPTQ 4-bit")
+ hf, extra = load_quant(GPTQ_PATH)
+ all_results["gptq_4bit"] = run_model("GPTQ", hf, processor, dataset)
+ unload(hf, extra)
+
+ print("[load] FOEM 3-bit")
+ hf, extra = load_quant(FOEM_PATH)
+ all_results["foem_3bit"] = run_model("FOEM", hf, processor, dataset)
+ unload(hf, extra)
+
+ # 질문/선택지 텍스트도 함께 저장 (이미지는 제외)
+ meta = {
+ ex["index"]: {
+ "question": ex["question"],
+ "choices": {l: ex[k] for l, k in zip(LETTERS, CHOICE_KEYS)},
+ "answer": ex["answer"],
+ "category": ex["category"],
+ }
+ for ex in dataset
+ }
+
+ with open(OUT_PATH, "w", encoding="utf-8") as f:
+ json.dump({"meta": meta, "results": all_results}, f, ensure_ascii=False, indent=2)
+ print(f"[done] saved -> {OUT_PATH}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/eval_kdtcbench.py b/FOEM/pseudo/eval_kdtcbench.py
new file mode 100644
index 0000000..3882218
--- /dev/null
+++ b/FOEM/pseudo/eval_kdtcbench.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env -S python -u
+"""K-DTCBench (한국어 문서/표/차트 VQA) 정확도 평가 (양자화 모델 vs BF16 원본).
+
+eval_kmmlu.py 와 동일한 이유로 generate() 대신 log-likelihood 기반 4지선다
+채점을 직접 구현한다: 공식 프롬프트로 채팅 템플릿을 구성한 뒤 [/INST] 직후
+"A"/"B"/"C"/"D" (공백 없음) 후보 토큰의 logit 을 비교해 argmax 선택.
+
+K-DTCBench 는 NCSOFT/K-DTCBench (test 단일 split, 240문항: document/table/chart
+각 80개, digital/handwritten 각 50%)이며 dev split 이 없어 zero-shot 평가만
+가능하다 (공식 벤치마크 프로토콜과 동일).
+
+ # 양자화 모델 (GPTQModel 포맷)
+ python eval_kdtcbench.py --model /workspace/LLM-VLM-in-Jetson/FOEM/Ministral-3-3B-Instruct-2512-BF16_gptq_4bit --quant
+ # BF16 원본
+ python eval_kdtcbench.py --model mistralai/Ministral-3-3B-Instruct-2512-BF16
+"""
+import argparse
+import os
+import time
+
+import torch
+from datasets import load_dataset
+from transformers import AutoProcessor
+
+CATEGORIES = ["document", "table", "chart"]
+CHOICE_LETTERS = ["A", "B", "C", "D"]
+CHOICE_KEYS = ["choice_a", "choice_b", "choice_c", "choice_d"]
+
+
+def load_model(path: str, quant: bool):
+ if quant:
+ from gptqmodel import GPTQModel
+ # sdpa 필수: eager 어텐션은 고해상도 문서 이미지에서 O(N^2) 어텐션 행렬이 커 OOM.
+ # 백엔드는 비트폭에 따라 다르게 실패한다 — 4bit 기본 backend(Marlin)는
+ # sm_120(Blackwell)에서 bf16 커널 JIT 컴파일 실패, 3bit는 TritonV2Linear가
+ # bits∈{2,4,8}만 지원. auto→gptq_triton→eager 순으로 순차 폴백.
+ last_err = None
+ for kwargs in (
+ {"attn_implementation": "sdpa"},
+ {"attn_implementation": "sdpa", "backend": "gptq_triton"},
+ {"attn_implementation": "eager", "backend": "gptq_triton"},
+ ):
+ try:
+ m = GPTQModel.load(path, **kwargs)
+ break
+ except Exception as e:
+ last_err = e
+ else:
+ raise last_err
+ hf = getattr(m, "model", m)
+ return hf, m
+ else:
+ from huggingface_hub import snapshot_download
+ from transformers import AutoConfig
+ p = snapshot_download(path, local_files_only=True) if "/" in path and not os.path.isdir(path) else path
+ cfg = AutoConfig.from_pretrained(p)
+ # BF16 베이스 모델은 tie_word_embeddings=True 상태로 저장돼 lm_head.weight가 체크포인트에 없음.
+ # False로 강제하면 lm_head가 랜덤 초기화되므로 원본 설정 그대로 유지.
+ import transformers as _t
+ cls = getattr(_t, cfg.architectures[0])
+ hf = cls.from_pretrained(
+ p, config=cfg, dtype=torch.bfloat16, device_map="auto", attn_implementation="sdpa"
+ )
+ hf.tie_weights() # 명시적으로 lm_head ← embed_tokens 결합 확인
+ return hf, hf
+
+
+def _build_prompt(ex: dict) -> str:
+ options = ", ".join(f"{l}: {ex[k]}" for l, k in zip(CHOICE_LETTERS, CHOICE_KEYS))
+ return (
+ f"{ex['question']}\nOptions: {options}\n\n"
+ "주어진 선택지 중 해당 옵션의 문자로 바로 답하세요."
+ )
+
+
+def _choice_token_ids(processor) -> list[int]:
+ # [/INST] 직후에는 공백 없이 바로 "A"/"B"/"C"/"D" 토큰이 온다 (" A" 등 공백 포함
+ # 토큰과는 다른 id). 실제 forward 로 top-token 이 이 매핑과 일치함을 확인함.
+ tok = processor.tokenizer
+ return [tok.encode(letter, add_special_tokens=False)[0] for letter in CHOICE_LETTERS]
+
+
+@torch.inference_mode()
+def evaluate_one(hf_model, processor, choice_ids: list[int], ex: dict) -> dict:
+ messages = [{"role": "user", "content": [
+ {"type": "image"},
+ {"type": "text", "text": _build_prompt(ex)},
+ ]}]
+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
+ inputs = processor(images=[ex["image"]], text=text, return_tensors="pt")
+ dev = next(hf_model.parameters()).device
+ inputs = {k: v.to(dev) for k, v in inputs.items()}
+
+ logits = hf_model(**inputs).logits[0, -1, :]
+ pred = int(torch.stack([logits[i] for i in choice_ids]).argmax())
+ answer_idx = CHOICE_LETTERS.index(ex["answer"])
+ return {"pred": CHOICE_LETTERS[pred], "answer": ex["answer"], "correct": pred == answer_idx}
+
+
+def evaluate_all(hf_model, processor, dataset, limit: int | None = None) -> dict:
+ choice_ids = _choice_token_ids(processor)
+ if limit is not None:
+ dataset = dataset.select(range(min(limit, len(dataset))))
+
+ per_category = {c: {"correct": 0, "total": 0} for c in CATEGORIES}
+ for i, ex in enumerate(dataset, 1):
+ r = evaluate_one(hf_model, processor, choice_ids, ex)
+ cat = ex["category"]
+ per_category[cat]["total"] += 1
+ if r["correct"]:
+ per_category[cat]["correct"] += 1
+ if i % 20 == 0 or i == len(dataset):
+ print(f"[kdtcbench] {i}/{len(dataset)} 완료 ({cat}: {r['pred']} vs {r['answer']})")
+
+ total_correct = sum(v["correct"] for v in per_category.values())
+ total_count = sum(v["total"] for v in per_category.values())
+ accuracy = total_correct / total_count if total_count else 0.0
+ cat_accs = [v["correct"] / v["total"] for v in per_category.values() if v["total"]]
+ macro_accuracy = sum(cat_accs) / len(cat_accs) if cat_accs else 0.0
+
+ for c in CATEGORIES:
+ v = per_category[c]
+ acc = v["correct"] / v["total"] if v["total"] else 0.0
+ print(f"[kdtcbench] {c}: {acc:.2%} ({v['correct']}/{v['total']})")
+
+ return {
+ "accuracy": accuracy,
+ "macro_accuracy": macro_accuracy,
+ "per_category": per_category,
+ "n_questions": total_count,
+ }
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--model", required=True)
+ ap.add_argument("--quant", action="store_true")
+ ap.add_argument("--processor", default=None)
+ ap.add_argument("--limit", type=int, default=None)
+ args = ap.parse_args()
+
+ processor = AutoProcessor.from_pretrained(args.processor or args.model)
+ hf, _ = load_model(args.model, args.quant)
+ hf.eval()
+
+ dataset = load_dataset("NCSOFT/K-DTCBench", split="test")
+
+ t0 = time.time()
+ result = evaluate_all(hf, processor, dataset, args.limit)
+ elapsed = time.time() - t0
+
+ print(f"RESULT\t{args.model}\tKDTCBench_Acc\t{result['accuracy']:.4f}")
+ print(f"[kdtcbench] micro={result['accuracy']:.2%} macro={result['macro_accuracy']:.2%} "
+ f"n={result['n_questions']} elapsed={elapsed/60:.1f}분")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/eval_kmmlu.py b/FOEM/pseudo/eval_kmmlu.py
new file mode 100644
index 0000000..f65aa77
--- /dev/null
+++ b/FOEM/pseudo/eval_kmmlu.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env -S python -u
+"""KMMLU (Korean MMLU) 정확도 평가 (양자화 모델 vs fp16 원본).
+
+eval_ppl.py 와 동일한 이유로 lm-eval 대신 log-likelihood 기반 4지선다 채점을
+직접 구현한다: 5-shot 프롬프트 뒤에 " A"/" B"/" C"/" D" 후보 토큰의 logit을
+비교해 argmax 선택 (lm-eval-harness MMLU/KMMLU 태스크와 동일한 방식).
+generate() 호출이 없어 멀티모달 래퍼와도 PPL 스크립트처럼 호환된다.
+
+HAERAE-HUB/KMMLU 는 단일 "All" config 가 없고 45개 과목별 config 만 존재하므로,
+전체 평가는 45개를 순회하며 누적한다.
+
+ # 양자화 모델 (GPTQModel 포맷), 전체 45개 과목, 5-shot
+ python eval_kmmlu.py --model quantized/Ministral-3-3B_gptq_4bit --quant
+ # 과목 1개만 빠르게 확인
+ python eval_kmmlu.py --model mistralai/Ministral-3-3B-Instruct-2512-BF16 \
+ --subset Korean-History --limit-per-subject 20 --shots 5
+"""
+import argparse
+import os
+import time
+
+import torch
+from datasets import load_dataset
+from transformers import AutoTokenizer
+
+KMMLU_SUBJECTS = [
+ "Accounting", "Agricultural-Sciences", "Aviation-Engineering-and-Maintenance",
+ "Biology", "Chemical-Engineering", "Chemistry", "Civil-Engineering",
+ "Computer-Science", "Construction", "Criminal-Law", "Ecology", "Economics",
+ "Education", "Electrical-Engineering", "Electronics-Engineering",
+ "Energy-Management", "Environmental-Science", "Fashion", "Food-Processing",
+ "Gas-Technology-and-Engineering", "Geomatics", "Health", "Industrial-Engineer",
+ "Information-Technology", "Interior-Architecture-and-Design", "Law",
+ "Machine-Design-and-Manufacturing", "Management", "Maritime-Engineering",
+ "Marketing", "Materials-Engineering", "Mechanical-Engineering",
+ "Nondestructive-Testing", "Patent", "Political-Science-and-Sociology",
+ "Psychology", "Public-Safety", "Railway-and-Automotive-Engineering",
+ "Real-Estate", "Refrigerating-Machinery", "Social-Welfare", "Taxation",
+ "Telecommunications-and-Wireless-Technology", "Korean-History", "Math",
+]
+
+CHOICE_LETTERS = ["A", "B", "C", "D"]
+
+
+def load_model(path: str, quant: bool):
+ if quant:
+ from gptqmodel import GPTQModel
+ m = GPTQModel.load(path, attn_implementation="sdpa")
+ hf = getattr(m, "model", m)
+ return hf, m
+ else:
+ from huggingface_hub import snapshot_download
+ from transformers import AutoConfig
+ p = snapshot_download(path, local_files_only=True) if "/" in path and not os.path.isdir(path) else path
+ cfg = AutoConfig.from_pretrained(p)
+ # BF16 베이스 모델은 tie_word_embeddings=True 상태로 저장돼 lm_head.weight가 체크포인트에 없음.
+ # False로 강제하면 lm_head가 랜덤 초기화되므로 원본 설정 그대로 유지.
+ import transformers as _t
+ cls = getattr(_t, cfg.architectures[0])
+ hf = cls.from_pretrained(
+ p, config=cfg, dtype=torch.bfloat16, device_map="auto", attn_implementation="sdpa"
+ )
+ hf.tie_weights() # 명시적으로 lm_head ← embed_tokens 결합 확인
+ return hf, hf
+
+
+def _format_choices(example: dict) -> str:
+ return "\n".join(f"{letter}. {example[letter]}" for letter in CHOICE_LETTERS)
+
+
+def _format_question(example: dict, with_answer: bool) -> str:
+ text = f"{example['question']}\n{_format_choices(example)}\n정답:"
+ if with_answer:
+ text += f" {CHOICE_LETTERS[example['answer'] - 1]}"
+ return text
+
+
+def build_few_shot_prefix(subject: str, shots: int) -> str:
+ if shots <= 0:
+ return ""
+ dev = load_dataset("HAERAE-HUB/KMMLU", subject, split="dev")
+ examples = dev.select(range(min(shots, len(dev))))
+ return "\n\n".join(_format_question(ex, with_answer=True) for ex in examples) + "\n\n"
+
+
+def _choice_token_ids(tokenizer) -> list[int]:
+ return [tokenizer.encode(f" {letter}", add_special_tokens=False)[0] for letter in CHOICE_LETTERS]
+
+
+@torch.inference_mode()
+def evaluate_subject(hf_model, tokenizer, subject: str, shots: int, limit: int | None = None) -> dict:
+ prefix = build_few_shot_prefix(subject, shots)
+ choice_ids = _choice_token_ids(tokenizer)
+ dev = next(hf_model.parameters()).device
+
+ test = load_dataset("HAERAE-HUB/KMMLU", subject, split="test")
+ if limit is not None:
+ test = test.select(range(min(limit, len(test))))
+
+ correct = 0
+ for ex in test:
+ prompt = prefix + _format_question(ex, with_answer=False)
+ input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(dev)
+ logits = hf_model(input_ids=input_ids).logits[0, -1, :]
+ pred = int(torch.stack([logits[i] for i in choice_ids]).argmax())
+ if pred == ex["answer"] - 1:
+ correct += 1
+
+ return {"correct": correct, "total": len(test)}
+
+
+def evaluate_all(hf_model, tokenizer, subjects: list[str], shots: int, limit: int | None = None) -> dict:
+ per_subject = {}
+ for i, subject in enumerate(subjects, 1):
+ r = evaluate_subject(hf_model, tokenizer, subject, shots, limit)
+ acc = r["correct"] / r["total"] if r["total"] else 0.0
+ per_subject[subject] = r
+ print(f"[kmmlu] {subject}: {acc:.2%} ({r['correct']}/{r['total']}) [{i}/{len(subjects)}]")
+
+ total_correct = sum(r["correct"] for r in per_subject.values())
+ total_count = sum(r["total"] for r in per_subject.values())
+ accuracy = total_correct / total_count if total_count else 0.0
+ subject_accs = [r["correct"] / r["total"] for r in per_subject.values() if r["total"]]
+ macro_accuracy = sum(subject_accs) / len(subject_accs) if subject_accs else 0.0
+
+ return {
+ "accuracy": accuracy,
+ "macro_accuracy": macro_accuracy,
+ "per_subject": per_subject,
+ "n_questions": total_count,
+ "shots": shots,
+ }
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--model", required=True)
+ ap.add_argument("--quant", action="store_true")
+ ap.add_argument("--tokenizer", default=None)
+ ap.add_argument("--subset", nargs="+", default=None,
+ help="평가할 과목 목록 (기본: KMMLU 45개 과목 전체)")
+ ap.add_argument("--shots", type=int, default=5)
+ ap.add_argument("--limit-per-subject", type=int, default=None)
+ args = ap.parse_args()
+
+ subjects = args.subset or KMMLU_SUBJECTS
+ tok = AutoTokenizer.from_pretrained(args.tokenizer or args.model)
+ hf, _ = load_model(args.model, args.quant)
+ hf.eval()
+
+ t0 = time.time()
+ result = evaluate_all(hf, tok, subjects, args.shots, args.limit_per_subject)
+ elapsed = time.time() - t0
+
+ print(f"RESULT\t{args.model}\tKMMLU_Acc\t{result['accuracy']:.4f}")
+ print(f"[kmmlu] micro={result['accuracy']:.2%} macro={result['macro_accuracy']:.2%} "
+ f"n={result['n_questions']} elapsed={elapsed/60:.1f}분")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/eval_ppl.py b/FOEM/pseudo/eval_ppl.py
new file mode 100755
index 0000000..f056cc5
--- /dev/null
+++ b/FOEM/pseudo/eval_ppl.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+"""WikiText-2 perplexity 평가 (양자화 모델 vs fp16 원본).
+
+lm-eval 의 모델 래퍼가 mistral3(멀티모달) 양자화 모델과 충돌할 수 있어,
+표준 sliding-window PPL 을 직접 계산한다. 지표 자체는 FOEM 논문이 보고하는
+WikiText perplexity 와 동일.
+
+ # 양자화 모델 (GPTQModel 포맷)
+ python eval_ppl.py --model quantized/MistralSmall3.1-24B-gptq-4bit --quant
+ # fp16 원본
+ python eval_ppl.py --model mistralai/Mistral-Small-3.1-24B-Instruct-2503
+"""
+import argparse
+import os
+
+import torch
+from datasets import load_dataset
+from transformers import AutoTokenizer
+
+
+def load_model(path: str, quant: bool):
+ if quant:
+ from gptqmodel import GPTQModel
+ m = GPTQModel.load(path, attn_implementation="sdpa")
+ hf = getattr(m, "model", m)
+ return hf, m
+ else:
+ from huggingface_hub import snapshot_download
+ from transformers import AutoConfig
+ p = snapshot_download(path, local_files_only=True) if "/" in path and not os.path.isdir(path) else path
+ # 멀티모달 등 비-CausalLM 도 처리: 아키텍처에서 클래스를 그대로 import.
+ # mistral3 처럼 lm_head 와 embed_tokens 가 양쪽 다 저장돼있는데
+ # tie_word_embeddings 기본값이 True 인 경우 lm_head 가 무시되어
+ # PPL 이 망가지므로 명시적으로 False 로 강제.
+ cfg = AutoConfig.from_pretrained(p)
+ cfg.tie_word_embeddings = False
+ if hasattr(cfg, "text_config"):
+ cfg.text_config.tie_word_embeddings = False
+ import transformers as _t
+ cls = getattr(_t, cfg.architectures[0])
+ hf = cls.from_pretrained(
+ p, config=cfg, dtype=torch.bfloat16, device_map="auto", attn_implementation="sdpa"
+ )
+ return hf, hf
+
+
+@torch.inference_mode()
+def ppl(hf_model, tokenizer, seqlen=2048):
+ test = load_dataset("Salesforce/wikitext", "wikitext-2-raw-v1", split="test")
+ enc = tokenizer("\n\n".join(test["text"]), return_tensors="pt").input_ids
+ dev = next(hf_model.parameters()).device
+ n = enc.size(1) // seqlen
+ nlls = []
+ for i in range(n):
+ batch = enc[:, i * seqlen:(i + 1) * seqlen].to(dev)
+ # 멀티모달 래퍼라도 input_ids 만 주면 language model 로짓이 나온다.
+ logits = hf_model(input_ids=batch).logits
+ shift_logits = logits[:, :-1, :].float()
+ shift_labels = batch[:, 1:]
+ loss = torch.nn.functional.cross_entropy(
+ shift_logits.reshape(-1, shift_logits.size(-1)), shift_labels.reshape(-1)
+ )
+ if torch.isfinite(loss):
+ nlls.append(loss * (seqlen - 1))
+ if not nlls:
+ return float("nan")
+ return torch.exp(torch.stack(nlls).sum() / (len(nlls) * (seqlen - 1))).item()
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--model", required=True)
+ ap.add_argument("--quant", action="store_true")
+ ap.add_argument("--seqlen", type=int, default=2048)
+ ap.add_argument("--tokenizer", default="mistralai/Mistral-Small-3.1-24B-Instruct-2503")
+ args = ap.parse_args()
+
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
+ hf, _ = load_model(args.model, args.quant)
+ hf.eval()
+ p = ppl(hf, tok, args.seqlen)
+ print(f"RESULT\t{args.model}\tWikiText2_PPL\t{p:.4f}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/quantize_mistral.py b/FOEM/pseudo/quantize_mistral.py
new file mode 100755
index 0000000..997090b
--- /dev/null
+++ b/FOEM/pseudo/quantize_mistral.py
@@ -0,0 +1,805 @@
+#!/usr/bin/env python
+"""Quantize VLM models (mistral3 family) with GPTQModel.
+
+일반 GPTQ 와 FOEM 을 같은 코드로 산출한다. 차이는 QuantizeConfig 에 foem= 인자 유무뿐.
+양자화 완료 후 저장 디렉토리 안에 README.md 를 자동 생성한다.
+
+저장 경로: /workspace/LLM-VLM-in-Jetson/FOEM/{모델명}_{method}_{bits}bit/
+
+ python quantize_mistral.py --method gptq --bits 4
+ python quantize_mistral.py --method foem --bits 4
+ python quantize_mistral.py --method foem --bits 3 --model mistralai/Ministral-3-3B-Instruct-2512-BF16
+"""
+import argparse
+import logging
+import os
+import re
+import statistics
+import time
+from datetime import datetime
+
+from datasets import load_dataset
+from huggingface_hub import snapshot_download
+
+from eval_kmmlu import KMMLU_SUBJECTS, evaluate_all
+from eval_kdtcbench import evaluate_all as evaluate_all_kdtc, load_model as load_model_kdtc
+from gptqmodel import GPTQModel, QuantizeConfig
+from gptqmodel.quantization.config import FORMAT
+
+try:
+ from gptqmodel import FOEMConfig
+except ImportError:
+ FOEMConfig = None
+
+BASE_OUT_DIR = "/workspace/LLM-VLM-in-Jetson/FOEM"
+
+
+# ── loss 캡처용 로그 핸들러 ──────────────────────────────────────────────────
+class _LossCapture(logging.Handler):
+ """gptqmodel 이 출력하는 per-module loss 행을 수집한다."""
+
+ # | foem | 0 | self_attn.q_proj | 3072, 4096 | bf16: 24.8MB | 4.985... |
+ _ROW_RE = re.compile(
+ r"\|\s*(gptq|foem)\s*\|\s*(\d+)\s*\|"
+ r"\s*([\w.]+)\s*\|"
+ r"\s*(\d+),\s*(\d+)\s*\|" # feat_in, feat_out
+ r"[^|]+\|"
+ r"\s*([\d.eE+\-]+)\s*\|"
+ )
+
+ def __init__(self):
+ super().__init__()
+ self.rows: list[dict] = []
+
+ def emit(self, record):
+ msg = record.getMessage()
+ m = self._ROW_RE.search(msg)
+ if m:
+ self.rows.append(
+ {
+ "layer": int(m.group(2)),
+ "module": m.group(3),
+ "feat_in": int(m.group(4)),
+ "feat_out": int(m.group(5)),
+ "loss": float(m.group(6)),
+ }
+ )
+
+
+def _attach_capture() -> _LossCapture:
+ cap = _LossCapture()
+ cap.setLevel(logging.DEBUG)
+ for name in ("gptqmodel", "root", ""):
+ logging.getLogger(name).addHandler(cap)
+ return cap
+
+
+# ── calibration ──────────────────────────────────────────────────────────────
+# 캘리브레이션 문서 하나가 비정상적으로 길면(수천~수만 토큰) eager 어텐션의
+# O(seqlen^2) 특성상 배치 크기와 무관하게 단일 샘플만으로도 OOM 을 유발할 수 있다.
+# c4 는 웹 크롤링이라 원래 짧은 경우가 대부분이지만, 허브 쪽 데이터가 갱신되며
+# 드물게 매우 긴 문서가 섞여 들어올 수 있음이 실제로 확인됐다 (alpha>0 실험 중
+# batch_size=1 에서도 7.33GiB 단일 할당 시도로 OOM). 소스에 관계없이 모든
+# 캘리브레이션 텍스트에 길이 캡을 일괄 적용해 이 문제를 원천 차단한다.
+CALIB_MAX_CHARS = 2000
+
+
+def get_calibration(nsamples: int, lang: str = "en") -> tuple[list[str], str]:
+ """returns (texts, dataset_label) — label is recorded in the README."""
+ if lang == "ko":
+ try:
+ import itertools
+ import random
+ # 항상 문서 처음(0번 글자)부터 자르면 256개 샘플이 전부 "OOO는 ~이다"
+ # 식 백과사전 서두 문체로 편중돼 c4(웹 크롤링, 문체 다양)보다 오히려 분포가
+ # 좁아진다. ① 스트림에서 매 STEP 번째 문서만 골라 주제 다양성을 확보하고,
+ # ② 각 문서 내에서도 무작위 시작 위치에서 잘라 본문 다양성을 확보한다.
+ STEP = 10
+ rng = random.Random(0)
+ ds = load_dataset("wikimedia/wikipedia", "20231101.ko", split="train", streaming=True)
+ texts = []
+ for ex in itertools.islice(ds, 0, nsamples * STEP, STEP):
+ t = ex["text"]
+ if not t.strip():
+ continue
+ start = rng.randint(0, max(0, len(t) - CALIB_MAX_CHARS))
+ texts.append(t[start:start + CALIB_MAX_CHARS])
+ if texts:
+ return texts, f"wikimedia/wikipedia (20231101.ko, every {STEP}th doc, random {CALIB_MAX_CHARS}자 crop)"
+ except Exception as e:
+ print(f"[calib] 한국어 위키 로드 실패 ({e}); 영어 c4 로 폴백")
+
+ try:
+ ds = load_dataset(
+ "allenai/c4",
+ data_files="en/c4-train.00001-of-01024.json.gz",
+ split="train",
+ )
+ texts = [t[:CALIB_MAX_CHARS] for t in ds.select(range(nsamples))["text"]]
+ return texts, f"allenai/c4 (en, {CALIB_MAX_CHARS}자 절단)"
+ except Exception as e:
+ print(f"[calib] c4 로드 실패 ({e}); wikitext2 로 폴백")
+ ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
+ texts = [t[:CALIB_MAX_CHARS] for t in ds["text"] if t.strip()]
+ return texts[:nsamples], f"wikitext-2 (fallback, {CALIB_MAX_CHARS}자 절단)"
+
+
+# ── README 생성 ───────────────────────────────────────────────────────────────
+def write_readme(out_dir: str, args, model_path: str, calib_len: int,
+ rows: list[dict], elapsed: float,
+ ppl_result: dict | None = None,
+ kmmlu_result: dict | None = None,
+ calib_label: str = "allenai/c4 (폴백: wikitext-2)",
+ kdtcbench_result: dict | None = None):
+ import torch
+
+ gpu_name = torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU"
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
+
+ # ── 통계 계산 ─────────────────────────────────────────────────────────────
+ module_types = [
+ "self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj",
+ "self_attn.o_proj", "mlp.gate_proj", "mlp.up_proj", "mlp.down_proj",
+ ]
+ per_mod: dict[str, list] = {m: [] for m in module_types}
+ per_layer: dict[int, list[float]] = {}
+ mod_shape: dict[str, tuple] = {} # module → (feat_in, feat_out) 대표값
+
+ for r in rows:
+ mod = r["module"]
+ if mod in per_mod:
+ per_mod[mod].append(r["loss"])
+ if mod not in mod_shape:
+ mod_shape[mod] = (r.get("feat_in", 0), r.get("feat_out", 0))
+ per_layer.setdefault(r["layer"], []).append(r["loss"])
+
+ all_losses = [r["loss"] for r in rows]
+ total_mods = len(rows)
+ avg_loss = statistics.mean(all_losses) if all_losses else 0.0
+ max_loss = max(all_losses) if all_losses else 0.0
+ min_loss = min(all_losses) if all_losses else 0.0
+
+ # ── 압축률 계산 ───────────────────────────────────────────────────────────
+ # 양자화된 선형 레이어 파라미터 수 (feat_in × feat_out 합산)
+ total_quant_params = sum(r.get("feat_in", 0) * r.get("feat_out", 0) for r in rows)
+ bf16_linear_gb = total_quant_params * 2 / 1e9 # BF16 = 2 bytes
+ theory_ratio = 16 / args.bits
+ group_overhead_mb = (total_quant_params / args.group_size) * 3 / 1e6 # scale(2B)+zero(1B)
+
+ safetensor_path = os.path.join(out_dir, "model.safetensors")
+ actual_gb = os.path.getsize(safetensor_path) / 1e9 if os.path.exists(safetensor_path) else 0.0
+ actual_ratio = bf16_linear_gb / actual_gb if actual_gb > 0 else 0.0
+
+ # ── 모듈별 테이블 ─────────────────────────────────────────────────────────
+ mod_table_rows = ""
+ for m in module_types:
+ v = per_mod[m]
+ if not v:
+ continue
+ fi, fo = mod_shape.get(m, (0, 0))
+ n_params = fi * fo
+ direction = "확장 ↑" if fo > fi else "축소 ↓"
+ avg_v = statistics.mean(v)
+ per_param = avg_v / n_params if n_params > 0 else 0.0
+ mod_table_rows += (
+ f"| `{m}` | {fi}→{fo} | {direction} | "
+ f"{avg_v:.2f} | {max(v):.2f} | {min(v):.5f} | {per_param:.2e} |\n"
+ )
+
+ # ── 레이어 bar ────────────────────────────────────────────────────────────
+ layer_bar = ""
+ if per_layer:
+ max_avg = max(statistics.mean(v) for v in per_layer.values())
+ for l in sorted(per_layer):
+ avg = statistics.mean(per_layer[l])
+ bar = "█" * max(1, int(avg / max_avg * 20))
+ layer_bar += f"| {l:2d} | {avg:7.2f} | {bar} |\n"
+
+ # ── 레이어 구간별 평균 ────────────────────────────────────────────────────
+ n_layers = max(per_layer.keys()) + 1 if per_layer else 1
+ zone_size = max(1, n_layers // 3)
+ zones = {
+ f"초기 (0~{zone_size-1})": [per_layer[l] for l in range(0, zone_size) if l in per_layer],
+ f"중간 ({zone_size}~{2*zone_size-1})": [per_layer[l] for l in range(zone_size, 2*zone_size) if l in per_layer],
+ f"후기 ({2*zone_size}~{n_layers-1})": [per_layer[l] for l in range(2*zone_size, n_layers) if l in per_layer],
+ }
+ zone_table = "| 구간 | 평균 loss | 역할 |\n|---|---:|---|\n"
+ roles = ["기본 어휘·문법 패턴 추출", "중간 추상화 (안정 구간)", "추론·맥락 이해, 고차원 표현"]
+ for (zone_name, zone_lists), role in zip(zones.items(), roles):
+ flat = [x for lst in zone_lists for x in lst]
+ avg = statistics.mean(flat) if flat else 0.0
+ zone_table += f"| {zone_name} | {avg:.1f} | {role} |\n"
+
+ # ── PPL 섹션 ──────────────────────────────────────────────────────────────
+ if ppl_result:
+ ppl_val = ppl_result["ppl"]
+ ppl_section = f"""
+## PPL 평가 결과
+
+| 항목 | 값 |
+|---|---|
+| 데이터셋 | {ppl_result["dataset"]} (test) |
+| 시퀀스 길이 | {ppl_result["seqlen"]} |
+| 슬라이딩 윈도우 수 | {ppl_result["n_windows"]} |
+| **PPL** | **{ppl_val:.4f}** |
+| 평가 소요 시간 | {ppl_result["elapsed"]/60:.1f} 분 |
+
+> **PPL (Perplexity)**: 언어 모델이 텍스트를 얼마나 잘 예측하는지 나타내는 지표.
+> 낮을수록 좋으며, 양자화 전후 PPL 차이가 클수록 품질 손실이 크다는 의미.
+> WikiText-2 는 국제 표준 벤치마크로, 동일 조건에서 서로 다른 양자화 방법 비교 시 사용한다.
+
+"""
+ else:
+ ppl_section = "\n> PPL 평가 생략 (--skip-ppl 옵션 사용)\n\n"
+
+ # ── KMMLU 섹션 ────────────────────────────────────────────────────────────
+ if kmmlu_result:
+ per_subject_rows = "\n".join(
+ f"| {subj} | {r['correct']/r['total']:.2%} | {r['total']} |"
+ for subj, r in kmmlu_result["per_subject"].items()
+ )
+ kmmlu_section = f"""
+## KMMLU 평가 결과
+
+| 항목 | 값 |
+|---|---|
+| 데이터셋 | KMMLU ({len(kmmlu_result["per_subject"])}개 과목, test) |
+| Shot | {kmmlu_result["shots"]}-shot |
+| 문항 수 | {kmmlu_result["n_questions"]} |
+| **정확도 (micro)** | **{kmmlu_result["accuracy"]:.2%}** |
+| 정확도 (macro, 과목 평균) | {kmmlu_result["macro_accuracy"]:.2%} |
+| 평가 소요 시간 | {kmmlu_result["elapsed"]/60:.1f} 분 |
+
+과목별 정확도
+
+| 과목 | 정확도 | 문항 수 |
+|---|---:|---:|
+{per_subject_rows}
+
+
+
+"""
+ else:
+ kmmlu_section = "\n> KMMLU 평가 생략 (--skip-kmmlu 옵션 사용)\n\n"
+
+ # ── K-DTCBench 섹션 ───────────────────────────────────────────────────────
+ if kdtcbench_result:
+ per_cat_rows = "\n".join(
+ f"| {cat} | {v['correct']/v['total']:.2%} | {v['total']} |"
+ for cat, v in kdtcbench_result["per_category"].items()
+ )
+ kdtcbench_section = f"""
+## K-DTCBench 평가 결과
+
+| 항목 | 값 |
+|---|---|
+| 데이터셋 | NCSOFT/K-DTCBench (document/table/chart, test) |
+| Shot | zero-shot |
+| 문항 수 | {kdtcbench_result["n_questions"]} |
+| **정확도 (micro=macro)** | **{kdtcbench_result["accuracy"]:.2%}** |
+| 평가 소요 시간 | {kdtcbench_result["elapsed"]/60:.1f} 분 |
+
+카테고리별 정확도
+
+| 카테고리 | 정확도 | 문항 수 |
+|---|---:|---:|
+{per_cat_rows}
+
+
+
+"""
+ else:
+ kdtcbench_section = "\n> K-DTCBench 평가 생략 (--skip-kdtcbench 옵션 사용)\n\n"
+
+ # ── worst 5 ───────────────────────────────────────────────────────────────
+ worst5 = sorted(rows, key=lambda x: x["loss"], reverse=True)[:5]
+ worst_table = "| 레이어 | 모듈 | loss |\n|---|---|---:|\n"
+ for r in worst5:
+ worst_table += f"| {r['layer']} | `{r['module']}` | {r['loss']:.4f} |\n"
+
+ # ── FOEM / GPTQ 알고리즘 섹션 ────────────────────────────────────────────
+ if args.method == "foem":
+ algo_section = f"""
+## FOEM 설정
+
+| 파라미터 | 값 |
+|---|---|
+| alpha | {args.alpha} |
+| beta | {args.beta} |
+
+- alpha=0.0 → 1차 보정(GPTAQ) 비활성화
+- beta={args.beta} → 직접 오차 피드백 활성화
+
+---
+
+## 양자화 심층 분석
+
+### 2. FOEM 알고리즘 원리
+
+FOEM(First-Order Error Matters, AAAI 2026)은 기본 GPTQ 가중치 갱신식에 오차 피드백 항을 추가한다.
+
+```
+ΔW = -e_i ⊗ H⁻¹[i,:] ← ① 기본 GPTQ 항
+ - (W - W_fp) · H⁻² · β ← ② FOEM 직접 오차 피드백 (β={args.beta})
+```
+
+| 기호 | 의미 |
+|---|---|
+| `e_i` | i번째 열 양자화 시 발생한 오차 |
+| `H` | 활성값 기반 Hessian 행렬 (XᵀX) |
+| `W_fp` | 누적 양자화 오차를 반영한 fp 가중치 |
+| `β` | 오차 피드백 강도 (이 실험: {args.beta}) |
+
+- **① GPTQ 항**: i번째 열 양자화 오차를 Hessian 역행렬로 나머지 열에 분산시켜 보상
+- **② FOEM β 항**: 이미 쌓인 누적 오차 `(W - W_fp)`를 다음 갱신에 직접 반영 → 오차가 전파되지 않고 흡수됨
+"""
+ else:
+ algo_section = f"""
+---
+
+## 양자화 심층 분석
+
+### 2. GPTQ 알고리즘 원리
+
+```
+ΔW = -e_i ⊗ H⁻¹[i,:] ← 기본 GPTQ 항
+```
+
+- `e_i`: i번째 열 양자화 시 발생한 오차
+- `H` (Hessian = XᵀX): 활성값 기반으로 각 가중치의 중요도를 반영
+- i번째 열을 양자화할 때 발생한 오차를 Hessian 역행렬을 통해 나머지 열에 분산시켜 보상
+"""
+
+ # ── README 본문 조합 ──────────────────────────────────────────────────────
+ readme = f"""# {os.path.basename(out_dir)}
+
+> 생성일: {now}
+
+## 모델 정보
+
+| 항목 | 값 |
+|---|---|
+| 원본 모델 | `{args.model}` |
+| 양자화 방법 | **{args.method.upper()}** |
+| 비트 | **{args.bits}-bit** |
+| group_size | {args.group_size} |
+| desc_act (act-order) | {args.desc_act} |
+| mixed_precision | {args.mixed_precision}{f" ({args.mixed_precision_bits}-bit)" if args.mixed_precision != "none" else ""} |
+| attn_impl | {args.attn} |
+| offload_to_disk | {args.offload_disk} |
+
+## 환경
+
+| 항목 | 값 |
+|---|---|
+| GPU | {gpu_name} |
+| gptqmodel | {_get_pkg_ver("gptqmodel")} |
+| transformers | {_get_pkg_ver("transformers")} |
+| torch | {_get_pkg_ver("torch")} |
+| 양자화 소요 시간 | {elapsed/60:.1f} 분 |
+
+## 캘리브레이션
+
+| 항목 | 값 |
+|---|---|
+| 데이터셋 | {calib_label} |
+| 샘플 수 | {calib_len} |
+| batch_size | {args.batch_size} |
+{algo_section}
+---
+{ppl_section}---
+{kmmlu_section}---
+{kdtcbench_section}---
+
+## 양자화 품질
+
+### 전체 통계
+
+| 항목 | 값 |
+|---|---|
+| 총 양자화 모듈 | {total_mods} |
+| RTN 폴백 | **0 / {total_mods}** (0%) |
+| 전체 평균 loss | {avg_loss:.4f} |
+| 전체 최대 loss | {max_loss:.4f} |
+| 전체 최소 loss | {min_loss:.6f} |
+
+### 모듈별 평균 loss
+
+| 모듈 | 입출력 | 방향 | 평균 loss | 최대 loss | 최소 loss | 파라미터당 loss |
+|---|---|---|---:|---:|---:|---:|
+{mod_table_rows}
+
+> **파라미터당 loss** = 평균 loss ÷ (feat_in × feat_out). 절대 loss가 커도 파라미터 수가 많으면 실제 영향은 작을 수 있음.
+
+### loss 상위 5개 모듈
+
+{worst_table}
+
+### 레이어별 평균 loss 추이
+
+| 레이어 | 평균 loss | 시각화 |
+|---|---:|---|
+{layer_bar}
+
+---
+
+### 1. 압축률 분석
+
+| 항목 | 값 |
+|---|---|
+| 원본 형식 | BF16 (16-bit) |
+| 양자화 비트 | {args.bits}-bit |
+| 이론 압축률 (선형 레이어) | 16 / {args.bits} = **{theory_ratio:.2f}×** |
+| 선형 레이어 BF16 추정 크기 | {bf16_linear_gb:.2f} GB |
+| 실제 모델 파일 크기 | {actual_gb:.2f} GB |
+| 실질 압축률 | **{actual_ratio:.1f}×** |
+| group_size={args.group_size} 오버헤드 | ~{group_overhead_mb:.0f} MB (scale+zero 파라미터) |
+
+> 이론 vs 실제 차이: 선형 레이어만 {args.bits}-bit 양자화되고, embed·lm_head·vision tower·norms는 BF16 유지.
+
+### 3. loss 지표 해석
+
+양자화 로그의 loss:
+
+```
+loss = ||WX - W_q X||²
+```
+
+| 기호 | 의미 |
+|---|---|
+| `W` | 원본 BF16 가중치 행렬 |
+| `W_q` | 양자화된 가중치 행렬 |
+| `X` | 캘리브레이션 입력 활성값 |
+
+**핵심**: 가중치 자체의 차이가 아닌 **실제 forward 출력의 차이**를 측정.
+활성값(X)이 크면 loss도 크게 나오므로 절대값보다 파라미터당 loss가 더 공정한 비교 지표.
+
+### 4. 모듈별 민감도 원인
+
+- **확장 방향** (gate_proj, up_proj, q_proj): 출력 차원이 커서 loss 절댓값이 크게 집계됨.
+ SwiGLU 구조에서 gate_proj는 sigmoid-like 게이트로 작용 → 작은 오차도 비선형적으로 증폭 가능.
+- **축소 방향** (down_proj, o_proj, k_proj, v_proj): 입력 공간에서 중요한 성분을 선택적으로 압축.
+ 상대적으로 양자화에 강인하며 파라미터당 loss가 낮음.
+
+### 5. 레이어 깊이 효과
+
+{zone_table}
+
+후기 레이어일수록 활성값 variance가 크고 Hessian 고유값 분포가 넓어짐
+→ {args.bits}-bit으로 표현해야 할 값의 범위가 넓어져 양자화 오차 급증.
+→ **모델이 "추론"을 담당하는 레이어일수록 양자화 손실이 크다.**
+
+### 6. 핵심 하이퍼파라미터 의미
+
+**damp_percent = 0.05**
+```
+H' = H + 0.05 × mean(diag(H)) × I
+```
+Hessian 역행렬 계산 수치 안정화용 정규화. 이 실험에서 RTN 폴백 0건으로 완벽히 작동.
+
+**group_size = {args.group_size}**
+
+| group_size | 오버헤드 | 품질 | 용도 |
+|---|---|---|---|
+| 32 | 높음 | 최상 | 고품질 우선 |
+| **{args.group_size}** | **중간** | **양호** | **이 실험 (표준값)** |
+| 256 | 낮음 | 보통 | 크기 우선 |
+
+---
+
+## 사용 방법
+
+```python
+from gptqmodel import GPTQModel
+
+model = GPTQModel.from_quantized("{out_dir}")
+```
+
+## 파일 구성
+
+| 파일 | 설명 |
+|---|---|
+| `model.safetensors` | 양자화된 가중치 ({actual_gb:.1f} GB) |
+| `quantize_config.json` | 양자화 설정 |
+| `config.json` | 모델 아키텍처 설정 |
+| `tokenizer.json` | 토크나이저 |
+| `README.md` | 이 파일 |
+"""
+
+ path = os.path.join(out_dir, "README.md")
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(readme)
+ print(f"[readme] saved -> {path}")
+
+
+# ── PPL 평가 ─────────────────────────────────────────────────────────────────
+def eval_ppl_quantized(out_dir: str, model_path: str, seqlen: int = 2048) -> dict:
+ """양자화 완료 직후 WikiText-2 PPL 을 측정한다.
+
+ quantize_mistral.py 내부에서 사용하기 위해 eval_ppl.py 의 로직을 인라인으로 가져옴.
+ GPTQModel 이 이미 메모리에 있지 않도록 저장된 out_dir 을 재로드하여 평가.
+ """
+ import math
+ import time
+
+ import torch
+ from datasets import load_dataset
+ from gptqmodel import GPTQModel
+ from transformers import AutoTokenizer
+
+ print(f"[ppl] 양자화 모델 로드: {out_dir}")
+ t0 = time.time()
+
+ # Marlin BF16 커널이 sm_120(Blackwell)에서 JIT 컴파일 실패 → Triton으로 폴백
+ try:
+ qm = GPTQModel.load(out_dir, attn_implementation="eager")
+ except Exception:
+ print("[ppl] 기본 백엔드 실패, gptq_triton 폴백 시도 ...")
+ qm = GPTQModel.load(out_dir, attn_implementation="eager", backend="gptq_triton")
+ hf = getattr(qm, "model", qm)
+ hf.eval()
+
+ try:
+ tok = AutoTokenizer.from_pretrained(model_path)
+ except Exception:
+ tok = AutoTokenizer.from_pretrained(out_dir)
+
+ print("[ppl] WikiText-2 test 로드 중 ...")
+ test = load_dataset("Salesforce/wikitext", "wikitext-2-raw-v1", split="test")
+ enc = tok("\n\n".join(test["text"]), return_tensors="pt").input_ids
+
+ dev = next(hf.parameters()).device
+ n = enc.size(1) // seqlen
+ nlls = []
+
+ print(f"[ppl] {n} 슬라이딩 윈도우 평가 시작 (seqlen={seqlen}) ...")
+ with torch.inference_mode():
+ for i in range(n):
+ batch = enc[:, i * seqlen:(i + 1) * seqlen].to(dev)
+ logits = hf(input_ids=batch).logits
+ shift_logits = logits[:, :-1, :].float()
+ shift_labels = batch[:, 1:]
+ loss = torch.nn.functional.cross_entropy(
+ shift_logits.reshape(-1, shift_logits.size(-1)),
+ shift_labels.reshape(-1),
+ )
+ if torch.isfinite(loss):
+ nlls.append(loss.item() * (seqlen - 1))
+ if (i + 1) % 10 == 0:
+ cur_ppl = math.exp(sum(nlls) / ((i + 1) * (seqlen - 1)))
+ print(f"[ppl] step {i+1}/{n} running PPL={cur_ppl:.2f}")
+
+ if nlls:
+ ppl_val = math.exp(sum(nlls) / (len(nlls) * (seqlen - 1)))
+ else:
+ ppl_val = float("nan")
+
+ elapsed = time.time() - t0
+ print(f"[ppl] WikiText-2 PPL = {ppl_val:.4f} ({elapsed/60:.1f} 분)")
+
+ # 평가 후 GPU 메모리 해제
+ del hf, qm
+ torch.cuda.empty_cache()
+
+ return {
+ "ppl": ppl_val,
+ "dataset": "WikiText-2",
+ "seqlen": seqlen,
+ "n_windows": len(nlls),
+ "elapsed": elapsed,
+ }
+
+
+# ── KMMLU 평가 ───────────────────────────────────────────────────────────────
+def eval_kmmlu_quantized(out_dir: str, model_path: str, shots: int = 5) -> dict:
+ """양자화 완료 직후 KMMLU (45개 과목) 정확도를 측정한다.
+
+ eval_ppl_quantized() 와 동일한 패턴: 저장된 out_dir 을 재로드하여 평가.
+ """
+ import time
+
+ import torch
+ from gptqmodel import GPTQModel
+ from transformers import AutoTokenizer
+
+ print(f"[kmmlu] 양자화 모델 로드: {out_dir}")
+ t0 = time.time()
+
+ try:
+ qm = GPTQModel.load(out_dir, attn_implementation="eager")
+ except Exception:
+ print("[kmmlu] 기본 백엔드 실패, gptq_triton 폴백 시도 ...")
+ qm = GPTQModel.load(out_dir, attn_implementation="eager", backend="gptq_triton")
+ hf = getattr(qm, "model", qm)
+ hf.eval()
+
+ try:
+ tok = AutoTokenizer.from_pretrained(model_path)
+ except Exception:
+ tok = AutoTokenizer.from_pretrained(out_dir)
+
+ print(f"[kmmlu] {len(KMMLU_SUBJECTS)}개 과목 평가 시작 ({shots}-shot) ...")
+ result = evaluate_all(hf, tok, KMMLU_SUBJECTS, shots)
+ result["elapsed"] = time.time() - t0
+ print(f"[kmmlu] micro={result['accuracy']:.2%} macro={result['macro_accuracy']:.2%} "
+ f"({result['elapsed']/60:.1f} 분)")
+
+ del hf, qm
+ torch.cuda.empty_cache()
+
+ return result
+
+
+# ── K-DTCBench 평가 ─────────────────────────────────────────────────────────
+def eval_kdtcbench_quantized(out_dir: str, model_path: str) -> dict:
+ """양자화 완료 직후 K-DTCBench (문서/표/차트 240문항) 정확도를 측정한다.
+
+ eval_kmmlu_quantized() 와 동일한 패턴: 저장된 out_dir 을 재로드하여 평가.
+ """
+ import time
+
+ import torch
+ from datasets import load_dataset as _load_dataset
+ from transformers import AutoProcessor
+
+ print(f"[kdtcbench] 양자화 모델 로드: {out_dir}")
+ t0 = time.time()
+
+ hf, qm = load_model_kdtc(out_dir, quant=True)
+ hf.eval()
+
+ try:
+ processor = AutoProcessor.from_pretrained(model_path)
+ except Exception:
+ processor = AutoProcessor.from_pretrained(out_dir)
+
+ dataset = _load_dataset("NCSOFT/K-DTCBench", split="test")
+ print(f"[kdtcbench] {len(dataset)}문항 평가 시작 (zero-shot) ...")
+ result = evaluate_all_kdtc(hf, processor, dataset)
+ result["elapsed"] = time.time() - t0
+ print(f"[kdtcbench] micro={result['accuracy']:.2%} macro={result['macro_accuracy']:.2%} "
+ f"({result['elapsed']/60:.1f} 분)")
+
+ del hf, qm
+ torch.cuda.empty_cache()
+
+ return result
+
+
+def _get_pkg_ver(pkg: str) -> str:
+ try:
+ import importlib.metadata
+ return importlib.metadata.version(pkg)
+ except Exception:
+ return "unknown"
+
+
+# ── main ──────────────────────────────────────────────────────────────────────
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--method", choices=["gptq", "foem"], required=True)
+ ap.add_argument("--bits", type=int, choices=[3, 4], required=True)
+ ap.add_argument(
+ "--model", default="mistralai/Mistral-Small-3.1-24B-Instruct-2503"
+ )
+ ap.add_argument("--nsamples", type=int, default=256)
+ ap.add_argument("--batch-size", type=int, default=4)
+ ap.add_argument("--group-size", type=int, default=128)
+ ap.add_argument("--desc-act", action="store_true", default=False,
+ help="act-order 활성화 (activation 크기 순으로 열을 재정렬 후 양자화 — 정확도↑, 시간↑)")
+ ap.add_argument("--calib-lang", default="en", choices=["en", "ko"],
+ help="캘리브레이션 코퍼스 언어 (ko: 한국어 위키피디아, 평가셋이 한국어일 때 도움)")
+ ap.add_argument("--alpha", type=float, default=0.0)
+ ap.add_argument("--beta", type=float, default=0.2)
+ # transformers 5.x SDPA 마스크의 meta-tensor .item() 버그 회피용.
+ ap.add_argument("--attn", default="eager", choices=["eager", "sdpa"])
+ # disk offload 를 켜면 embed_tokens 출력이 meta tensor 가 되어 입력 캡처가 깨짐.
+ ap.add_argument("--offload-disk", action="store_true", default=False)
+ ap.add_argument("--out", default=None)
+ ap.add_argument("--skip-ppl", action="store_true", default=False,
+ help="PPL 평가 건너뜀 (시간 절약)")
+ ap.add_argument("--ppl-seqlen", type=int, default=2048)
+ ap.add_argument("--skip-kmmlu", action="store_true", default=False,
+ help="KMMLU 평가 건너뜀 (45개 과목, 시간이 오래 걸림)")
+ ap.add_argument("--kmmlu-shots", type=int, default=5)
+ ap.add_argument("--skip-kdtcbench", action="store_true", default=False,
+ help="K-DTCBench 평가 건너뜀")
+ ap.add_argument("--mixed-precision", choices=["none", "modules", "layers"], default="none",
+ help="modules: mlp.gate_proj/up_proj만 --mixed-precision-bits 로 상향, "
+ "layers: 마지막 --mixed-precision-last-layers 개 레이어 전체를 상향")
+ ap.add_argument("--mixed-precision-bits", type=int, default=4)
+ ap.add_argument("--mixed-precision-last-layers", type=int, default=6)
+ ap.add_argument("--format", choices=["gptq", "gptq_v2"], default="gptq",
+ help="gptq_v2: 레거시 v1 qzeros 변환을 건너뜀 (dynamic bits 혼합 시 v1 변환 버그 회피용)")
+ args = ap.parse_args()
+
+ base_name = args.model.split("/")[-1]
+ out = args.out or os.path.join(
+ BASE_OUT_DIR, f"{base_name}_{args.method}_{args.bits}bit"
+ )
+
+ if args.method == "foem" and FOEMConfig is None:
+ raise SystemExit(
+ "설치된 gptqmodel 에 FOEMConfig 가 없습니다. `pip install -U gptqmodel` 후 재시도."
+ )
+
+ if os.path.isdir(args.model):
+ model_path = args.model
+ else:
+ model_path = snapshot_download(args.model, local_files_only=True)
+ print(f"[model] local path: {model_path}")
+
+ qcfg = dict(bits=args.bits, group_size=args.group_size, offload_to_disk=args.offload_disk,
+ desc_act=args.desc_act,
+ format=FORMAT.GPTQ_V2 if args.format == "gptq_v2" else FORMAT.GPTQ)
+ if args.method == "foem":
+ qcfg["foem"] = FOEMConfig(alpha=args.alpha, beta=args.beta, device="auto")
+
+ dynamic = None
+ if args.mixed_precision == "modules":
+ # gate_proj/up_proj 는 SwiGLU 게이트 역할로 파라미터당 loss 가 가장 커 4-bit 로 상향.
+ dynamic = {
+ r"+:model\.language_model\.layers\.\d+\.mlp\.(gate_proj|up_proj)$":
+ {"bits": args.mixed_precision_bits},
+ }
+ elif args.mixed_precision == "layers":
+ # 후기 레이어(추론/맥락 이해 담당)일수록 loss 가 급증하는 경향이 있어
+ # 마지막 N개 레이어 전체를 4-bit 로 상향.
+ from transformers import AutoConfig
+ n_layers = AutoConfig.from_pretrained(model_path).text_config.num_hidden_layers
+ start = n_layers - args.mixed_precision_last_layers
+ dynamic = {
+ rf"+:model\.language_model\.layers\.({'|'.join(str(i) for i in range(start, n_layers))})\..*":
+ {"bits": args.mixed_precision_bits},
+ }
+ if dynamic is not None:
+ qcfg["dynamic"] = dynamic
+
+ quant_config = QuantizeConfig(**qcfg)
+ print(f"[config] {qcfg}")
+
+ calib, calib_label = get_calibration(args.nsamples, lang=args.calib_lang)
+ print(f"[calib] {len(calib)} samples from {calib_label}")
+
+ cap = _attach_capture()
+ t0 = time.time()
+ model = GPTQModel.load(model_path, quant_config, attn_implementation=args.attn)
+ model.quantize(calib, batch_size=args.batch_size)
+ elapsed = time.time() - t0
+
+ os.makedirs(out, exist_ok=True)
+ model.save(out)
+ print(f"[done] saved -> {out}")
+
+ # 양자화 모델 메모리 해제 후 PPL 평가
+ del model
+ import torch
+ torch.cuda.empty_cache()
+
+ ppl_result = None
+ if not args.skip_ppl:
+ try:
+ ppl_result = eval_ppl_quantized(out, model_path, seqlen=args.ppl_seqlen)
+ except Exception as e:
+ print(f"[ppl] 평가 실패 (README 에 생략으로 기록): {e}")
+
+ kmmlu_result = None
+ if not args.skip_kmmlu:
+ try:
+ kmmlu_result = eval_kmmlu_quantized(out, model_path, shots=args.kmmlu_shots)
+ except Exception as e:
+ print(f"[kmmlu] 평가 실패 (README 에 생략으로 기록): {e}")
+
+ kdtcbench_result = None
+ if not args.skip_kdtcbench:
+ try:
+ kdtcbench_result = eval_kdtcbench_quantized(out, model_path)
+ except Exception as e:
+ print(f"[kdtcbench] 평가 실패 (README 에 생략으로 기록): {e}")
+
+ write_readme(out, args, model_path, len(calib), cap.rows, elapsed, ppl_result, kmmlu_result,
+ calib_label=calib_label, kdtcbench_result=kdtcbench_result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/serve_foem.py b/FOEM/pseudo/serve_foem.py
new file mode 100644
index 0000000..c527f19
--- /dev/null
+++ b/FOEM/pseudo/serve_foem.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env -S python -u
+"""FOEM 3bit 베이스라인 모델을 FastAPI로 서빙한다.
+
+vLLM은 GPTQ 3-bit를 지원하지 않고(Unsupported quantization config: bits=3) 이
+모델(Mistral3 VLM)의 멀티모달 프로세서 경로에서 자체 버그까지 겹쳐 서빙이
+불가능했다. 대신 이 세션 내내 검증된 HF transformers + GPTQModel 로딩 방식을
+그대로 쓰되, 매 요청마다 재로딩하지 않도록 FastAPI lifespan에서 모델을 한 번만
+로드해 상주시킨다.
+
+ uvicorn serve_foem:app --host 0.0.0.0 --port 8000
+"""
+import os
+import sys
+import time
+from contextlib import asynccontextmanager
+
+import torch
+from fastapi import FastAPI
+from fastapi.responses import HTMLResponse
+from pydantic import BaseModel
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from eval_kdtcbench import load_model # noqa: E402 (양자화 모델 3단계 폴백 로더 재사용)
+from transformers import AutoProcessor # noqa: E402
+
+MODEL_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/Ministral-3-3B-Instruct-2512-BF16_foem_3bit"
+TOKENIZER_PATH = (
+ "/root/.cache/huggingface/hub/models--mistralai--Ministral-3-3B-Instruct-2512-BF16"
+ "/snapshots/ecc3ba8b43a45610e709327c049d24b009bfec88"
+)
+
+state: dict = {}
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ print(f"[load] {MODEL_PATH}")
+ t0 = time.time()
+ hf, _ = load_model(MODEL_PATH, quant=True)
+ hf.eval()
+ processor = AutoProcessor.from_pretrained(TOKENIZER_PATH)
+ state["model"] = hf
+ state["processor"] = processor
+ print(f"[load] done ({time.time() - t0:.1f}s, device={next(hf.parameters()).device})")
+ yield
+ state.clear()
+
+
+app = FastAPI(title="FOEM 3bit Korean chat", lifespan=lifespan)
+
+
+class ChatRequest(BaseModel):
+ message: str
+ max_tokens: int = 1024
+ temperature: float = 0.7
+
+
+class ChatResponse(BaseModel):
+ response: str
+ elapsed_sec: float
+
+
+@app.get("/health")
+def health():
+ return {"status": "ok", "model": MODEL_PATH, "loaded": "model" in state}
+
+
+@app.get("/", response_class=HTMLResponse)
+def index():
+ return """
+
+
+
+
+FOEM 3bit 한국어 챗
+
+
+
+FOEM 3bit 베이스라인 (group_size=128) — 한국어 질의
+
+
+
+
+
+
+
+
+
+"""
+
+
+@app.post("/chat", response_model=ChatResponse)
+@torch.inference_mode()
+def chat(req: ChatRequest):
+ hf = state["model"]
+ processor = state["processor"]
+ dev = next(hf.parameters()).device
+
+ messages = [{"role": "user", "content": req.message}]
+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
+ inputs = processor(text=text, return_tensors="pt").to(dev)
+
+ t0 = time.time()
+ out = hf.generate(
+ **inputs,
+ max_new_tokens=req.max_tokens,
+ do_sample=req.temperature > 0,
+ temperature=req.temperature if req.temperature > 0 else 1.0,
+ top_p=0.9,
+ )
+ gen_tokens = out[0][inputs["input_ids"].shape[1]:]
+ response_text = processor.tokenizer.decode(gen_tokens, skip_special_tokens=True)
+ elapsed = time.time() - t0
+
+ return ChatResponse(response=response_text, elapsed_sec=elapsed)
diff --git a/FOEM/pseudo/serve_foem_client_test.py b/FOEM/pseudo/serve_foem_client_test.py
new file mode 100644
index 0000000..4b3c31d
--- /dev/null
+++ b/FOEM/pseudo/serve_foem_client_test.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env -S python -u
+"""serve_foem.py 서버에 기본 한국어 질의를 보내고 응답을 로그로 남긴다."""
+import json
+import time
+
+import requests
+
+BASE_URL = "http://127.0.0.1:8000"
+LOG_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/logs/serve_foem_korean_queries.log"
+
+PROMPTS = [
+ "대한민국의 수도는 어디인가요?",
+ "김치찌개를 맛있게 끓이는 방법을 3단계로 간단히 설명해줘.",
+ "인공지능이 무엇인지 한 문단으로 설명해줘.",
+ "1부터 10까지 더하면 얼마야?",
+ "오늘 기분이 좋아. 짧은 한국어 시를 하나 지어줘.",
+]
+
+
+def main():
+ r = requests.get(f"{BASE_URL}/health", timeout=10)
+ print("[health]", r.json())
+
+ results = []
+ for i, prompt in enumerate(PROMPTS, 1):
+ t0 = time.time()
+ r = requests.post(f"{BASE_URL}/chat", json={"message": prompt}, timeout=120)
+ r.raise_for_status()
+ data = r.json()
+ wall = time.time() - t0
+ print(f"\n=== [{i}/{len(PROMPTS)}] server={data['elapsed_sec']:.1f}s wall={wall:.1f}s ===")
+ print(f"Q: {prompt}")
+ print(f"A: {data['response']}")
+ results.append({
+ "question": prompt,
+ "answer": data["response"],
+ "server_elapsed_sec": data["elapsed_sec"],
+ "wall_elapsed_sec": wall,
+ })
+
+ with open(LOG_PATH, "w", encoding="utf-8") as f:
+ json.dump({
+ "model": "FOEM 3bit baseline (group_size=128, alpha=0.0, beta=0.2)",
+ "serving": "FastAPI + HF transformers generate() (model loaded once at startup)",
+ "results": results,
+ }, f, ensure_ascii=False, indent=2)
+ print(f"\n[done] saved -> {LOG_PATH}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/update_report_with_base.py b/FOEM/pseudo/update_report_with_base.py
new file mode 100644
index 0000000..2a64dc2
--- /dev/null
+++ b/FOEM/pseudo/update_report_with_base.py
@@ -0,0 +1,327 @@
+#!/usr/bin/env python
+"""base_kmmlu.log에서 결과를 파싱해 KMMLU_REPORT.md를 베이스 모델 비교 버전으로 재작성."""
+import ast
+import re
+import sys
+
+LOG_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/logs/base_kmmlu.log"
+REPORT_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/pseudo/KMMLU_REPORT.md"
+
+# 기존 GPTQ / FOEM 결과 (하드코딩)
+GPTQ = {
+ "label": "GPTQ 4-bit",
+ "ppl": 8.72,
+ "micro": 44.19,
+ "macro": 43.42,
+ "elapsed_min": 62.4,
+ "n": 35030,
+ "per_subject": {
+ "Accounting": (37.00, 100), "Agricultural-Sciences": (36.10, 1000),
+ "Aviation-Engineering-and-Maintenance": (41.50, 1000), "Biology": (36.90, 1000),
+ "Chemical-Engineering": (47.30, 1000), "Chemistry": (48.00, 600),
+ "Civil-Engineering": (41.50, 1000), "Computer-Science": (69.20, 1000),
+ "Construction": (34.40, 1000), "Criminal-Law": (33.50, 200),
+ "Ecology": (45.80, 1000), "Economics": (46.92, 130),
+ "Education": (58.00, 100), "Electrical-Engineering": (34.30, 1000),
+ "Electronics-Engineering": (52.90, 1000), "Energy-Management": (33.10, 1000),
+ "Environmental-Science": (30.40, 1000), "Fashion": (44.60, 1000),
+ "Food-Processing": (41.90, 1000), "Gas-Technology-and-Engineering": (36.40, 1000),
+ "Geomatics": (39.80, 1000), "Health": (55.00, 100),
+ "Industrial-Engineer": (40.50, 1000), "Information-Technology": (64.40, 1000),
+ "Interior-Architecture-and-Design": (50.80, 1000), "Law": (42.60, 1000),
+ "Machine-Design-and-Manufacturing": (42.80, 1000), "Management": (49.00, 1000),
+ "Maritime-Engineering": (44.83, 600), "Marketing": (74.90, 1000),
+ "Materials-Engineering": (44.80, 1000), "Mechanical-Engineering": (39.40, 1000),
+ "Nondestructive-Testing": (45.40, 1000), "Patent": (32.00, 100),
+ "Political-Science-and-Sociology": (47.33, 300), "Psychology": (43.20, 1000),
+ "Public-Safety": (38.10, 1000), "Railway-and-Automotive-Engineering": (38.60, 1000),
+ "Real-Estate": (38.50, 200), "Refrigerating-Machinery": (32.70, 1000),
+ "Social-Welfare": (52.60, 1000), "Taxation": (40.00, 200),
+ "Telecommunications-and-Wireless-Technology": (55.50, 1000),
+ "Korean-History": (27.00, 100), "Math": (24.33, 300),
+ },
+}
+
+FOEM = {
+ "label": "FOEM 3-bit",
+ "ppl": 10.87,
+ "micro": 35.58,
+ "macro": 35.03,
+ "elapsed_min": 64.9,
+ "n": 35030,
+ "per_subject": {
+ "Accounting": (37.00, 100), "Agricultural-Sciences": (28.00, 1000),
+ "Aviation-Engineering-and-Maintenance": (35.30, 1000), "Biology": (28.80, 1000),
+ "Chemical-Engineering": (37.00, 1000), "Chemistry": (36.50, 600),
+ "Civil-Engineering": (31.30, 1000), "Computer-Science": (54.10, 1000),
+ "Construction": (32.30, 1000), "Criminal-Law": (27.50, 200),
+ "Ecology": (31.40, 1000), "Economics": (39.23, 130),
+ "Education": (37.00, 100), "Electrical-Engineering": (26.40, 1000),
+ "Electronics-Engineering": (37.90, 1000), "Energy-Management": (27.50, 1000),
+ "Environmental-Science": (25.20, 1000), "Fashion": (35.10, 1000),
+ "Food-Processing": (34.40, 1000), "Gas-Technology-and-Engineering": (30.30, 1000),
+ "Geomatics": (32.60, 1000), "Health": (37.00, 100),
+ "Industrial-Engineer": (33.00, 1000), "Information-Technology": (49.10, 1000),
+ "Interior-Architecture-and-Design": (41.20, 1000), "Law": (33.30, 1000),
+ "Machine-Design-and-Manufacturing": (34.40, 1000), "Management": (41.60, 1000),
+ "Maritime-Engineering": (35.17, 600), "Marketing": (59.90, 1000),
+ "Materials-Engineering": (35.50, 1000), "Mechanical-Engineering": (32.10, 1000),
+ "Nondestructive-Testing": (36.60, 1000), "Patent": (25.00, 100),
+ "Political-Science-and-Sociology": (38.00, 300), "Psychology": (34.60, 1000),
+ "Public-Safety": (28.90, 1000), "Railway-and-Automotive-Engineering": (31.90, 1000),
+ "Real-Estate": (36.50, 200), "Refrigerating-Machinery": (28.20, 1000),
+ "Social-Welfare": (38.60, 1000), "Taxation": (32.50, 200),
+ "Telecommunications-and-Wireless-Technology": (41.30, 1000),
+ "Korean-History": (33.00, 100), "Math": (23.67, 300),
+ },
+}
+
+
+def parse_log(path: str) -> dict:
+ per_subject = {}
+ micro = macro = elapsed_min = n_questions = None
+
+ with open(path) as f:
+ for line in f:
+ # [kmmlu] Subject: 44.00% (44/100) [3/45]
+ m = re.match(r"\[kmmlu\] (.+?): ([\d.]+)%\s+\((\d+)/(\d+)\)", line)
+ if m:
+ subj, acc, corr, total = m.group(1), float(m.group(2)), int(m.group(3)), int(m.group(4))
+ per_subject[subj] = (acc, total)
+
+ # [kmmlu] micro=XX% macro=XX% n=XXXXX elapsed=XX.Xmin
+ m2 = re.match(r"\[kmmlu\] micro=([\d.]+)%\s+macro=([\d.]+)%\s+n=(\d+)\s+elapsed=([\d.]+)분", line)
+ if m2:
+ micro = float(m2.group(1))
+ macro = float(m2.group(2))
+ n_questions = int(m2.group(3))
+ elapsed_min = float(m2.group(4))
+
+ return {
+ "micro": micro,
+ "macro": macro,
+ "elapsed_min": elapsed_min,
+ "n": n_questions,
+ "per_subject": per_subject,
+ }
+
+
+def format_diff(base_val, comp_val):
+ d = base_val - comp_val
+ sign = "+" if d > 0 else ""
+ return f"{sign}{d:.1f}%p"
+
+
+def main():
+ base = parse_log(LOG_PATH)
+ if base["micro"] is None:
+ print("ERROR: 평가 결과를 파싱할 수 없습니다. 로그를 확인하세요.", file=sys.stderr)
+ sys.exit(1)
+
+ base["label"] = "BF16 (원본)"
+ base["ppl"] = "N/A"
+
+ print(f"[파싱 완료] micro={base['micro']:.2f}% macro={base['macro']:.2f}% "
+ f"elapsed={base['elapsed_min']:.1f}분 subjects={len(base['per_subject'])}")
+
+ subjects = list(GPTQ["per_subject"].keys())
+
+ # -- 과목별 3-way 비교 표 --
+ subject_rows = []
+ for s in subjects:
+ b_acc, n = base["per_subject"].get(s, (0.0, 0))
+ g_acc = GPTQ["per_subject"][s][0]
+ f_acc = FOEM["per_subject"][s][0]
+ subject_rows.append((s, b_acc, g_acc, f_acc, n))
+
+ # GPTQ 대비 base 차이 기준 정렬
+ subject_rows_sorted = sorted(subject_rows, key=lambda r: r[1] - r[2], reverse=True)
+
+ top5_base_wins = subject_rows_sorted[:5]
+ bottom5 = subject_rows_sorted[-5:]
+
+ # -- 보고서 작성 --
+ base_micro = base["micro"]
+ base_macro = base["macro"]
+ base_elapsed = base["elapsed_min"]
+ base_n = base["n"]
+
+ report = f"""# Ministral-3-3B KMMLU 평가 비교 보고서
+**BF16 원본 vs GPTQ 4-bit vs FOEM 3-bit (5-shot, KMMLU 45개 과목 전체)**
+
+작성일: 2026-07-01 · 모델: `mistralai/Ministral-3-3B-Instruct-2512-BF16`
+
+---
+
+## 1. 목적
+
+WikiText-2 perplexity와 KMMLU 정확도를 세 가지 모델 변형에 걸쳐 비교한다: ① **BF16 원본** (비양자화, 기준선), ② **GPTQ 4-bit**, ③ **FOEM 3-bit** (α=0, β=0.2). 이를 통해 각 양자화 방법이 한국어 도메인 지식·추론 능력을 얼마나 보존/손실시키는지 정량화한다.
+
+| 모델 | 양자화 방법 | 비트 | WikiText-2 PPL |
+|---|---|---|---|
+| `Ministral-3-3B-Instruct-2512-BF16` | 없음 (기준) | BF16 | N/A |
+| `Ministral-3-3B-Instruct-2512-BF16_gptq_4bit` | GPTQ | 4-bit | 8.72 |
+| `Ministral-3-3B-Instruct-2512-BF16_foem_3bit` | FOEM (α=0, β=0.2) | 3-bit | 10.87 |
+
+---
+
+## 2. 평가 방법
+
+- **데이터셋**: `HAERAE-HUB/KMMLU` — 단일 "All" config 없음, 45개 과목별 config 순회 누적.
+- **Few-shot**: 5-shot (각 과목 `dev` split 5문항, KMMLU 공식 벤치마크와 동일 설정).
+- **채점 방식**: log-likelihood 기반 4지선다 (generate() 없음). 5-shot 프롬프트 뒤에서 " A"/" B"/" C"/" D" 토큰의 logit argmax.
+- **구현**: `pseudo/eval_kmmlu.py`. BF16 원본은 `Mistral3ForConditionalGeneration.from_pretrained()` + `tie_weights()` 호출, 양자화 모델은 `GPTQModel.load()` + `.model` 추출 패턴.
+
+---
+
+## 3. 전체 결과
+
+| 모델 | 정확도 (micro) | 정확도 (macro) | 소요 시간 | 문항 수 |
+|---|---:|---:|---:|---:|
+| **BF16 원본** | **{base_micro:.2f}%** | {base_macro:.2f}% | {base_elapsed:.1f}분 | {base_n:,} |
+| **GPTQ 4-bit** | **{GPTQ['micro']:.2f}%** | {GPTQ['macro']:.2f}% | {GPTQ['elapsed_min']:.1f}분 | {GPTQ['n']:,} |
+| **FOEM 3-bit** | **{FOEM['micro']:.2f}%** | {FOEM['macro']:.2f}% | {FOEM['elapsed_min']:.1f}분 | {FOEM['n']:,} |
+| 4지선다 무작위 기준선 | 25.00% | 25.00% | - | - |
+
+**양자화로 인한 정확도 저하 (BF16 대비)**
+
+| 모델 | micro 차이 | macro 차이 |
+|---|---:|---:|
+| GPTQ 4-bit | {GPTQ['micro'] - base_micro:+.2f}%p | {GPTQ['macro'] - base_macro:+.2f}%p |
+| FOEM 3-bit | {FOEM['micro'] - base_micro:+.2f}%p | {FOEM['macro'] - base_macro:+.2f}%p |
+
+---
+
+## 4. 과목별 분석
+
+45개 과목 전체 3-way 비교 표
+
+| 과목 | BF16 | GPTQ 4-bit | FOEM 3-bit | 문항 수 |
+|---|---:|---:|---:|---:|
+"""
+
+ for s, b, g, f, n in subject_rows:
+ report += f"| {s} | {b:.1f}% | {g:.1f}% | {f:.1f}% | {n} |\n"
+
+ report += """
+
+
+### BF16 대비 GPTQ가 크게 저하된 과목 (top 5 손실)
+
+| 과목 | BF16 | GPTQ | 차이 | 문항 수 |
+|---|---:|---:|---:|---:|
+"""
+ # BF16 - GPTQ 손실이 큰 순 (BF16이 더 높은 것)
+ loss_gptq = sorted(subject_rows, key=lambda r: r[2] - r[1])[:5]
+ for s, b, g, f, n in loss_gptq:
+ report += f"| {s} | {b:.1f}% | {g:.1f}% | {g-b:+.1f}%p | {n} |\n"
+
+ report += """
+### BF16 대비 FOEM이 크게 저하된 과목 (top 5 손실)
+
+| 과목 | BF16 | FOEM | 차이 | 문항 수 |
+|---|---:|---:|---:|---:|
+"""
+ loss_foem = sorted(subject_rows, key=lambda r: r[3] - r[1])[:5]
+ for s, b, g, f, n in loss_foem:
+ report += f"| {s} | {b:.1f}% | {f:.1f}% | {f-b:+.1f}%p | {n} |\n"
+
+ n_base_gt_gptq = sum(1 for _, b, g, _, _ in subject_rows if b > g)
+ n_base_gt_foem = sum(1 for _, b, _, f, _ in subject_rows if b > f)
+ report += f"""
+- BF16이 GPTQ를 앞서는 과목: **{n_base_gt_gptq}/45개**, BF16이 FOEM을 앞서는 과목: **{n_base_gt_foem}/45개**.
+
+---
+
+## 5. 실제 질의 / 추론 예시
+
+*(이전 보고서의 4개 예시 — GPTQ/FOEM 비교 — 를 유지. 베이스 모델 예측값은 별도 재실행 시 추가 가능.)*
+
+### 5-1. Korean-History (두 모델 모두 오답, FOEM이 더 근접)
+
+> **질문**: 밑줄 친 '왕'의 재위 기간에 있었던 사실로 옳은 것은? 왕은 노론과 소론, 남인을 두루 등용하였으며 젊은 관료들을 재교육하기 위해 초계문신제를 시행하였다. 또 서얼 출신의 유능한 인사를 규장각 검서관으로 등용하였다.
+> - A. 동학이 창시되었다.
+> - B. 대전회통이 편찬되었다.
+> - C. 신해통공이 시행되었다.
+> - D. 홍경래의 난이 발생하였다.
+>
+> **정답: C** (정조 시대 — 신해통공 시행)
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| GPTQ 4-bit | **B** | 18.8% | 45.2% | 14.7% | 21.3% | ✗ |
+| FOEM 3-bit | **B** | 17.9% | 29.5% | 23.0% | 29.5% | ✗ |
+
+### 5-2. Math (두 모델 모두 오답)
+
+> **질문**: 함수 f(x)=-x³+2x+3에 대하여 직선 y=-x+k와 곡선 y=f(x)의 그래프가 서로 다른 두 점에서 만나도록 하는 모든 양수 k의 합은?
+> - A. 6 B. 12 C. 18 D. 36
+>
+> **정답: A**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| GPTQ 4-bit | **B** | 12.6% | 34.2% | 26.6% | 26.6% | ✗ |
+| FOEM 3-bit | **D** | 18.6% | 27.0% | 23.8% | 30.6% | ✗ |
+
+### 5-3. Computer-Science (두 모델 모두 정답, 고확신)
+
+> **질문**: NTFS 파일시스템에서 부팅과정에서 읽어들이는 부분으로 디스크에 저장되어 있는 파일의 정보DB를 담아 놓은 것은?
+> - A. VFAT B. MFT C. PROM D. CMOS
+>
+> **정답: B**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| GPTQ 4-bit | **B** | 0.2% | **97.9%** | 1.4% | 0.6% | ✓ |
+| FOEM 3-bit | **B** | 14.5% | **69.3%** | 8.8% | 7.3% | ✓ |
+
+### 5-4. Marketing (두 모델 모두 오답, FOEM 과확신)
+
+> **질문**: 표본을 통해서 모집단의 성질을 정확히 추론하기 위하여 고려할 사항으로 중요하지 않은 것은?
+> - A. 모집단 요소들의 동질성 정도 B. 표본의 크기 C. 표본선정 시기 D. 표본조사 예산
+>
+> **정답: C**
+
+| 모델 | 예측 | A | B | C | D | 정오 |
+|---|---|---:|---:|---:|---:|---|
+| GPTQ 4-bit | **D** | 1.6% | 1.8% | 28.4% | 68.2% | ✗ |
+| FOEM 3-bit | **D** | 3.7% | 1.3% | 4.8% | **90.2%** | ✗ |
+
+---
+
+## 6. 결론
+
+"""
+ # 동적 결론
+ gptq_loss = GPTQ['micro'] - base_micro
+ foem_loss = FOEM['micro'] - base_micro
+ gptq_loss_macro = GPTQ['macro'] - base_macro
+ foem_loss_macro = FOEM['macro'] - base_macro
+
+ report += f"""1. **BF16 원본({base_micro:.2f}%)이 기준선**이며, GPTQ 4-bit는 {gptq_loss:+.2f}%p, FOEM 3-bit는 {foem_loss:+.2f}%p 차이를 보인다. 비트 수가 더 낮은 FOEM 3-bit의 손실이 더 크며, 이는 PPL 결과(8.72 vs 10.87)와 같은 방향이다.
+2. **GPTQ vs FOEM 알고리즘 비교**: GPTQ 4-bit가 FOEM 3-bit보다 micro +{GPTQ['micro']-FOEM['micro']:.2f}%p 높지만, 이는 알고리즘 차이가 아닌 **비트 수 차이(4 vs 3)**가 주된 원인일 가능성이 높다. 공정한 비교를 위해 동일 비트(4-bit FOEM vs 4-bit GPTQ, 3-bit FOEM vs 3-bit GPTQ) 실험이 필요하다.
+3. **Math 과목**은 세 모델 모두 무작위 기준선(25%) 수준으로, 양자화 손실보다 **3B 베이스 모델의 수리 추론 한계**가 더 크게 작용한다.
+4. **Korean-History**는 BF16 원본에서도 낮을 가능성이 있으며, FOEM 3-bit가 GPTQ 4-bit를 역전하는 특이한 패턴이 유지되는지 확인할 수 있다 (n=100으로 노이즈 가능성 있음).
+5. **후속 실험 제안**: ① 동일 비트 FOEM vs GPTQ 비교; ② Math 과목을 위한 chain-of-thought 프롬프트 실험; ③ Calibration curve (confidence vs accuracy) 분석으로 과확신(overconfident) 오답 패턴 정량화.
+
+---
+
+## 7. 산출물
+
+- `Ministral-3-3B-Instruct-2512-BF16_gptq_4bit/README.md` — `## KMMLU 평가 결과` 섹션 포함.
+- `Ministral-3-3B-Instruct-2512-BF16_foem_3bit/README.md` — 동일.
+- `pseudo/eval_kmmlu.py` — 독립 실행형 KMMLU 평가 스크립트 (BF16 원본 및 양자화 모델 모두 지원).
+- `pseudo/quantize_mistral.py` — `eval_kmmlu_quantized()` 통합.
+- `pseudo/KMMLU_REPORT.md` — 이 파일 (3-way 비교, 베이스 모델 포함).
+"""
+
+ with open(REPORT_PATH, "w") as f:
+ f.write(report)
+
+ print(f"[done] {REPORT_PATH} 업데이트 완료")
+ print(f" BF16 micro={base_micro:.2f}% GPTQ micro={GPTQ['micro']:.2f}% FOEM micro={FOEM['micro']:.2f}%")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/FOEM/pseudo/vllm_korean_smoketest.py b/FOEM/pseudo/vllm_korean_smoketest.py
new file mode 100644
index 0000000..b04cfea
--- /dev/null
+++ b/FOEM/pseudo/vllm_korean_smoketest.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env -S python -u
+"""vLLM으로 FOEM 4bit 베이스라인 모델을 서빙해 기본 한국어 질의 응답을 로그로 남긴다.
+
+FOEM 3bit는 vLLM의 GPTQ 백엔드가 3-bit를 지원하지 않아 로드 자체가 불가능함이
+확인됨 (`Unsupported quantization config: bits=3, sym=True`). 동일 베이스라인
+설정(alpha=0.0, beta=0.2, group_size=128)으로 4-bit 재양자화해 대체."""
+import json
+import time
+
+from vllm import LLM, SamplingParams
+
+MODEL_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/Ministral-3-3B-Instruct-2512-BF16_foem_4bit"
+# 양자화 디렉토리에는 프로세서/토크나이저 부속 설정(processor_config.json 등)이 없어
+# vLLM 로딩 시 여러 호환성 에러가 남 (eval_kdtcbench.py 등 기존 스크립트와 동일하게
+# 원본 BF16 스냅샷에서 토크나이저를 읽고, 가중치만 양자화 디렉토리에서 읽는다).
+TOKENIZER_PATH = "/root/.cache/huggingface/hub/models--mistralai--Ministral-3-3B-Instruct-2512-BF16/snapshots/ecc3ba8b43a45610e709327c049d24b009bfec88"
+LOG_PATH = "/workspace/LLM-VLM-in-Jetson/FOEM/logs/vllm_korean_smoketest.log"
+
+PROMPTS = [
+ "대한민국의 수도는 어디인가요?",
+ "김치찌개를 맛있게 끓이는 방법을 3단계로 간단히 설명해줘.",
+ "인공지능이 무엇인지 한 문단으로 설명해줘.",
+ "1부터 10까지 더하면 얼마야?",
+ "오늘 기분이 좋아. 짧은 한국어 시를 하나 지어줘.",
+]
+
+
+def main():
+ t0 = time.time()
+ print(f"[load] {MODEL_PATH}")
+ llm = LLM(
+ model=MODEL_PATH,
+ quantization="gptq",
+ dtype="bfloat16",
+ gpu_memory_utilization=0.85,
+ max_model_len=4096,
+ enforce_eager=True,
+ trust_remote_code=True,
+ tokenizer_mode="mistral",
+ )
+ print(f"[load] done ({time.time()-t0:.1f}s)")
+
+ sampling_params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=256)
+
+ results = []
+ for i, prompt in enumerate(PROMPTS, 1):
+ messages = [{"role": "user", "content": prompt}]
+ t1 = time.time()
+ outputs = llm.chat([messages], sampling_params)
+ elapsed = time.time() - t1
+ text = outputs[0].outputs[0].text
+ print(f"\n=== [{i}/{len(PROMPTS)}] ({elapsed:.1f}s) ===")
+ print(f"Q: {prompt}")
+ print(f"A: {text}")
+ results.append({"question": prompt, "answer": text, "elapsed_sec": elapsed})
+
+ with open(LOG_PATH, "w", encoding="utf-8") as f:
+ json.dump({
+ "model": MODEL_PATH,
+ "quantization": "FOEM 3bit baseline (group_size=128, alpha=0.0, beta=0.2)",
+ "results": results,
+ }, f, ensure_ascii=False, indent=2)
+ print(f"\n[done] saved -> {LOG_PATH}")
+
+
+if __name__ == "__main__":
+ main()