refactor(phase-01): v3 retune fast & balanced roles

fast (Gemma 4 26B-A4B):
- Enable mmproj GPU loading (vision ~1s, 12x faster than CPU)
- KV f16 → q8_0 (save ~2.5 GB VRAM for mmproj)
- Tensor split 0.5,0.5 → 0.43,0.57 (13/17 layers)
- Remove --mlock/--poll/--prio/-t/-tb (no measurable impact)
- measured_tps 74.65 → 71.89 (trade 3.7% speed for vision)

balanced (Qwen 3.5 35B-A3B):
- Tensor split 0.5,0.5 → 0.48,0.52 (enables pipeline parallelism)
- Ubatch 128 → 256 (prefill +78%: 649 → 1,157 t/s)
- mmproj + --no-mmproj-offload (CPU vision, VRAM headroom)
- Remove useless flags same as fast
- measured_tps 61.62 → 64.16 (+4.1%)

Other:
- Document full retuning in docs/v3_{fast,balanced}_retuning_log.md
- Session report at .planning/reports/20260411-session-report.md
- Add bench utilities: bench_short/bench_long/test_ts_ratios
- Speculative decoding (E2B draft) experimented but rejected
  (+14% gen vs -31% cold start + tokenizer mismatch + mmproj conflict)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Variet-Worker
2026-04-11 14:55:27 +09:00
parent 219985b9ce
commit 0dee779a73
9 changed files with 1135 additions and 80 deletions

View File

@@ -3,8 +3,8 @@ gsd_state_version: 1.0
milestone: v1.1 milestone: v1.1
milestone_name: milestone milestone_name: milestone
status: planning status: planning
last_updated: "2026-04-08T01:58:00.000Z" last_updated: "2026-04-11T10:30:00.000Z"
last_activity: 2026-04-08 last_activity: 2026-04-11
progress: progress:
total_phases: 3 total_phases: 3
completed_phases: 2 completed_phases: 2
@@ -32,7 +32,7 @@ Last activity: 2026-04-08
## Completed Phases ## Completed Phases
- Phase 01 (LLM Tuning): 5개 모델 최적 설정 확정 (74.65 / 61.62 / 16.0 / 16.7 / 8.95 t/s) - Phase 01 (LLM Tuning): 5개 모델 최적 설정 확정 (71.89‡ / 64.16† / 16.0 / 16.7 / 8.95 t/s) — † balanced / ‡ fast 2026-04-11 재튜닝
- Phase 02 (API Engine): Variet Engine v1.0 — FastAPI 프록시 + 핫스왑 + 503 보호 - Phase 02 (API Engine): Variet Engine v1.0 — FastAPI 프록시 + 핫스왑 + 503 보호
## Recent Decisions ## Recent Decisions
@@ -43,6 +43,8 @@ Last activity: 2026-04-08
- Variet Engine: 단일 포트(8000) FastAPI 리버스 프록시. - Variet Engine: 단일 포트(8000) FastAPI 리버스 프록시.
- config/engine_models.json → 모든 설정의 Single Source of Truth. - config/engine_models.json → 모든 설정의 Single Source of Truth.
- CLI-First 검증 전략: VS Code Extension 전 OpenClaude CLI로 에이전트 루프 먼저 검증. - CLI-First 검증 전략: VS Code Extension 전 OpenClaude CLI로 에이전트 루프 먼저 검증.
- balanced 역할 v3 재튜닝 (2026-04-11): `-ub 256`, `-ts 0.48,0.52`, `--no-mmproj-offload`, 보조 옵션(mlock/poll/prio) 제거. 실측 61.62 → 64.16 t/s. prefill 649 → 1,157 t/s (+78%). 상세: `docs/v3_balanced_retuning_log.md`.
- fast 역할 v3 재튜닝 (2026-04-11): `cache-type q8_0`, `-ts 0.43,0.57`, `--mmproj` GPU 적재, 보조 옵션 제거. 실측 74.65 → 71.89 t/s (-3.7%). Vision GPU 지원 추가 (이미지 인코딩 ~1초). Speculative Decoding (E2B draft) 실험 후 채택 안 함. 상세: `docs/v3_fast_retuning_log.md`.
## Roadmap Evolution ## Roadmap Evolution
@@ -58,6 +60,5 @@ None.
## Session Continuity ## Session Continuity
Last session: 2026-04-08T10:58:00+09:00 Last session: 2026-04-11T10:30:00+09:00
Stopped at: Phase 05 PLAN created, user will execute manually Stopped at: balanced 역할 v3 재튜닝 완료 — config/engine_models.json, docs/v3_balanced_retuning_log.md, Phase 01 VERIFICATION.md 증적 저장 완료. 다음 작업 선택 대기.
Resume file: .planning/phases/05-vscode-extension-packaging/.continue-here.md

View File

@@ -4,16 +4,16 @@
| # | 모델 | 역할 | 실측 속도 | 컨텍스트 | GPU 구성 | 상태 | | # | 모델 | 역할 | 실측 속도 | 컨텍스트 | GPU 구성 | 상태 |
|:-:|------|------|:--------:|:-------:|----------|:----:| |:-:|------|------|:--------:|:-------:|----------|:----:|
| 1 | Gemma 4 26B-A4B Q4_K_M | fast | 74.65 t/s | 256K | 듀얼 | ✅ | | 1 | Gemma 4 26B-A4B Q4_K_M | fast | 71.89 t/s | 256K | 듀얼 | ✅ |
| 2 | Qwen 3.5 35B-A3B Q4_K_M | balanced | 61.62 t/s | 256K | 듀얼 | ✅ | | 2 | Qwen 3.5 35B-A3B Q4_K_M | balanced | 64.16 t/s | 256K | 듀얼 | ✅ |
| 3 | Gemma 4 31B Dense Q4_K_M | deep-coder | 16.0 t/s | 192K | 듀얼 | ✅ | | 3 | Gemma 4 31B Dense Q4_K_M | deep-coder | 16.0 t/s | 192K | 듀얼 | ✅ |
| 4 | Qwen 3.5 27B Dense Q4_K_M | deep-logic | 16.7 t/s | 256K | 듀얼 | ✅ | | 4 | Qwen 3.5 27B Dense Q4_K_M | deep-logic | 16.7 t/s | 256K | 듀얼 | ✅ |
| 5 | Qwen 3.5 122B-A10B Q4_K_M | ultra | 8.95 t/s | 256K | GPU 1만 | ✅ | | 5 | Qwen 3.5 122B-A10B Q4_K_M | ultra | 8.95 t/s | 256K | GPU 1만 | ✅ |
## UAT 기준 달성 여부 ## UAT 기준 달성 여부
- [x] Fast tier ≥ 70 t/s → **74.65 t/s** - [x] Fast tier ≥ 70 t/s → **71.89 t/s**
- [x] Balanced tier ≥ 50 t/s → **61.62 t/s** - [x] Balanced tier ≥ 50 t/s → **64.16 t/s**
- [x] Deep tier 안정 구동 → **16.0 / 16.7 t/s** - [x] Deep tier 안정 구동 → **16.0 / 16.7 t/s**
- [x] Ultra tier 구동 가능 → **8.95 t/s** - [x] Ultra tier 구동 가능 → **8.95 t/s**
- [x] 모든 모델 VRAM 12GB x 2 이내 → ✅ - [x] 모든 모델 VRAM 12GB x 2 이내 → ✅
@@ -21,3 +21,29 @@
## Phase Status: ✅ COMPLETE ## Phase Status: ✅ COMPLETE
완료 일시: 2026-04-07 완료 일시: 2026-04-07
---
**balanced 역할 재튜닝 (2026-04-11):**
이후 세션에서 `balanced` (Qwen3.5-35B-A3B) 설정을 실측 기반으로 재검증. 기존 Phase 01 튜닝은 단일 GPU 환경(`found 1 CUDA devices` 로그로 확인)에서 수행됐고, 이후 듀얼 GPU + mmproj 옵션 추가로 설정이 drift된 상태였음. v3 재튜닝 결과 **61.62 → 64.16 t/s** (+4.1%) 달성. 상세 과정/증적은 [`docs/v3_balanced_retuning_log.md`](../../../docs/v3_balanced_retuning_log.md) 참조.
**주요 변경점:**
- `-ub 128 → 256` (prefill +78%, 긴 프롬프트 3,100 tok 기준 649 → 1,157 t/s)
- `-ts 0.5,0.5 → 0.48,0.52` (compute buffer 여유 확보)
- `--mlock`, `--poll 50`, `--prio 3` 제거 (실측 영향 0.04 t/s)
- `--mmproj` + `--no-mmproj-offload` 추가 (비전 기능 유지 + VRAM 858 MiB 확보)
- GPU 0 PCIe 3.0 x4 슬롯 병목 진단 → Gen 속도 62 t/s 상한 원인 규명
---
**fast 역할 재튜닝 (2026-04-11):**
`fast` (Gemma 4 26B-A4B) 설정도 동일 세션에서 재검증. Phase 01 측정치 74.65는 단일 GPU 조건. 듀얼 GPU + vision 지원 추가 과정에서 **71.89 t/s**로 확정 (-3.7%). 상세 과정/증적은 [`docs/v3_fast_retuning_log.md`](../../../docs/v3_fast_retuning_log.md) 참조.
**주요 변경점:**
- `--cache-type-k/v: f16 → q8_0` (KV 절약 ~2.5 GB로 mmproj GPU 수용)
- `--mmproj models/gemma-4-26B-mmproj-F16.gguf` 신규 추가 (Vision 지원, GPU 적재)
- `-ts 0.43,0.57` (13/17 레이어 분할, mmproj 수용 공간 확보)
- `-ub 512 -b 2048` (유지, 스윗스팟 재확인)
- `--mlock`, `--poll 50`, `--prio 3`, `-t 6`, `-tb 6` 제거
- Speculative Decoding(E2B draft) 실험 후 **채택 안 함** — +14% 이득 vs 복잡성/cold start 페널티
- Vision GPU 인코딩 ~1초 (640×640 image, 283 tokens)

View File

@@ -0,0 +1,176 @@
# GSD Session Report
**Generated:** 2026-04-11
**Project:** Variet LLM (2+0 GPU 로컬 AI 어시스턴트)
**Milestone:** v1.1 — OpenClaude CLI Integration (Phase 01 유지보수)
---
## Session Summary
**Duration:** Single session (2026-04-11)
**Phase Progress:** Phase 01 — 재튜닝 완료 (balanced + fast 갱신)
**Plans Executed:** 0 (정규 plan 실행 없음, 유지보수 세션)
**Commits Made:** 0 (미커밋, 세션 후 `/commit` 예정)
**Files Changed:** 5 신규/수정 + 2 삭제
---
## Session Type
**유형:** Ad-hoc 유지보수 세션 (scheduled plan 외)
**범위:** Phase 01(LLM Tuning) 재검증 — `balanced`(Qwen3.5-35B-A3B), `fast`(Gemma 4 26B-A4B) 두 역할 재튜닝
이전 세션들에서 엔진 구성/플래그가 drift되어 원래 측정치(61.62 / 74.65 t/s) 재현이 어려운 상태였음. 실측 기반으로 설정 재확정.
---
## Work Performed
### 1. balanced 역할 (Qwen 3.5 35B-A3B) 재튜닝
**주요 발견:**
- Gemma4 커뮤니티에선 잘 안 알려진 **Gated Delta Net (SSM/Mamba) 하이브리드 구조**로 40레이어 중 **10개만 full attention**. KV 캐시 초기 추정 5GB → 실제 1.4GB
- GPU 0은 PCIe 3.0 x4 슬롯 (3.94 GB/s), GPU 1은 PCIe 4.0 x16 (31.5 GB/s) — **1/8 비대칭**
- Pipeline Parallelism 수동 OFF 불가, VRAM 초과 시 자동 fallback
- 이전 drift된 설정들(mmproj-F16.gguf placeholder, 보조 옵션) 정리
**변경 내역:**
```diff
- --mmproj models/mmproj-F16.gguf (drift 상태)
+ --mmproj models/mmproj-F16.gguf (의도적)
+ --no-mmproj-offload (CPU 오프로드)
- -ub 128 → -ub 256 (prefill +78%)
- -ts 0.5,0.5 → -ts 0.48,0.52 (PP 활성화 가능, 14/16 layer split)
- (remove) --mlock --poll 50 --prio 3 (영향 0.04 t/s)
- measured_tps: 61.62 → 64.16
```
**실측 결과:**
- Text 짧은 프롬프트: 64.16 t/s (+4.1% vs 레퍼런스)
- Text 긴 프롬프트 prefill: 1,157 t/s (vs 이전 649 t/s, +78%)
- Vision CPU 인코딩: 6.4초 (640×640)
### 2. fast 역할 (Gemma 4 26B-A4B) 재튜닝
**주요 발견:**
- 30레이어 중 5개만 full attention (매 6번째). SWA와 interleave
- Phase 01 측정치 74.65 t/s는 **단일 GPU 기준** (archive 로그 `found 1 CUDA devices` 확인)
- mmproj GPU 적재 가능성 검증 → `-ts 0.43,0.57`에서 안정 동작 확인
- **Speculative Decoding (E2B draft) 실험**: +14% gen 이득 vs 복잡성/cold start 페널티 → **채택 안 함**
- Run 1 cold start: 49.68 t/s (기본 72 대비 -31%)
- Tokenizer 불일치 경고 (E2B vs 26B)
- mmproj와 병행 불가
**변경 내역:**
```diff
- --cache-type-k/v f16 → q8_0 (VRAM 2.5GB 절약, mmproj 수용)
+ --mmproj models/gemma-4-26B-mmproj-F16.gguf (다운로드 + GPU 적재)
- -ts (default) → -ts 0.43,0.57 (13/17 split)
- (remove) --mlock --poll 50 --prio 3 -t 6 -tb 6
- measured_tps: 74.65 → 72.04
```
**실측 결과:**
- Text 짧은 프롬프트: 71.89 t/s (BEST 72.91)
- Text 긴 프롬프트 prefill: 1,672 t/s, gen 66.67 t/s
- Vision GPU 인코딩: ~1초 (640×640) — CPU 대비 **~12배 빠름**
### 3. Speculative Decoding 실험 (채택 안 함)
Gemma 4 E2B draft model 다운로드 후 검증:
- 다운로드: `mmproj-F16.gguf` (Qwen3.5용, 858MB), `gemma-4-26B-mmproj-F16.gguf` (1.19GB), `gemma-4-E2B-it-Q4_K_M.gguf` (2.9GB)
- Draft acceptance rate: 82% (일반 텍스트 기준)
- Gen 속도 BEST: 86.18 t/s (+18% vs 72.04)
- Gen 평균 (Run 1 제외): 82.18 t/s (+14%)
- **결정: 채택 안 함** — cold start -31%, 복잡성 5+ flag, mmproj 비호환
- 세션 마지막 E2B 파일 삭제 (3.8 GB 회수)
### 4. 파일 변경 내역
**신규/수정:**
- [config/engine_models.json](../../config/engine_models.json) — balanced & fast 역할 최종 확정
- [docs/v3_balanced_retuning_log.md](../../docs/v3_balanced_retuning_log.md) — **신규**
- [docs/v3_fast_retuning_log.md](../../docs/v3_fast_retuning_log.md) — **신규**
- [.planning/phases/01-llm-tuning/VERIFICATION.md](../phases/01-llm-tuning/VERIFICATION.md) — balanced † + fast ‡ 재튜닝 주석
- [.planning/STATE.md](../STATE.md) — Recent Decisions / Session Continuity 갱신
- [scripts/test_ts_ratios.py](../../scripts/test_ts_ratios.py) — 신규 유틸
- [scripts/bench_long.py](../../scripts/bench_long.py) — 신규 유틸 (긴 프롬프트 벤치)
- [scripts/bench_short.py](../../scripts/bench_short.py) — 신규 유틸 (짧은 프롬프트 + 비전 벤치)
**삭제:**
- `models/gemma-4-E2B-it-Q4_K_M.gguf` (2.9 GB)
- `models/gemma-4-E2B-mmproj-F16.gguf` (940 MB)
---
## Outcomes
### Phase 01 재검증 결과
| 역할 | 이전 | v3 (2026-04-11) | 차이 | 비고 |
|------|------|-----------------|-----|-----|
| fast (Gemma 4 26B) | 74.65 | **71.89** | -3.7% | Vision GPU 추가 |
| balanced (Qwen 3.5 35B) | 61.62 | **64.16** | +4.1% | prefill +78% |
| deep-coder | 16.0 | 16.0 | — | 변경 없음 |
| deep-logic | 16.7 | 16.7 | — | 변경 없음 |
| ultra | 8.95 | 8.95 | — | 변경 없음 |
### 하드웨어 제약 문서화
- **GPU 0 PCIe 3.0 x4 bottleneck** 공식 진단 (Gen 속도 상한 원인)
- **Pipeline Parallelism 자동 fallback** 동작 이해 (수동 제어 불가)
- **Gemma 4 E2B PLE (Per-Layer Embeddings)** 구조로 2.9 GB 파일이 GPU 1.4 GB만 점유
### 속도 테스트 방법론 개선
- Python `time.time()` 기반 HTTP 왕복 측정 → llama.cpp 내부 `timings` 필드 사용으로 정확도 향상
- 긴 프롬프트(3,100 tok) vs 짧은 프롬프트(170 tok) 이원 측정
- `scripts/bench_short.py`, `scripts/bench_long.py` 재사용 가능 벤치 유틸 확립
---
## Decisions Made
| 결정 | 근거 |
|------|-----|
| balanced `-ub 256 -ts 0.48,0.52 --no-mmproj-offload` 확정 | 실측 스윕 결과 PP 활성화 스윗스팟 |
| fast `-ub 512 -ts 0.43,0.57 q8_0 mmproj GPU` 확정 | Vision GPU 이득 12배 + VRAM 안정 |
| Speculative Decoding 채택 안 함 | +14% 이득 vs 복잡성/cold-start/mmproj 비호환 |
| fast KV f16 → q8_0 전환 | mmproj 수용 공간 확보, 품질 손실 미미 |
| `--mlock/--poll 50/--prio 3/-t/-tb` 제거 (두 역할 모두) | 실측 영향 오차 범위, 전용 추론기 환경 |
| E2B draft 모델 파일 삭제 | 미채택, 3.8 GB 디스크 회수 |
---
## Blockers / Concerns
**없음.** Phase 01 재튜닝 완료. Phase 06(Hermes Agent)은 이미 완료 상태, 세션 진입 시점과 무관.
### 알려진 구조적 제약
- **GPU 0 PCIe 3.0 x4 병목**: Gen 속도 62-72 t/s 상한의 주원인. 소프트웨어로 해결 불가, 하드웨어 재구성 필요
- **Pipeline Parallelism 수동 OFF 불가**: VRAM 초과 시 자동 fallback. `-np 1` 단일 사용자 환경에선 PP 실질 이득 없음
- **llama.cpp `--main-gpu`가 mmproj 위치 제어 안 됨**: 이 빌드에서 확인, 항상 CUDA0에 로드
---
## Next Actions
1. **`/commit`** — 변경사항 4개 + 삭제 기록 커밋
2. Phase 01 재튜닝 종료, Phase 06(Hermes Agent) 완료 상태 유지
3. (향후) milestone v1.1 남은 작업 `/gsd-resume-work`로 복귀
---
## Token Usage Estimate
**대략치:** 본 세션은 대규모 벤치마크 반복이 많았고, 긴 로그 파일 반복 그렙 및 설정 파일 수차례 편집이 포함되어 컨텍스트 상당량 소모.
- 파일 편집: ~15회
- 스크립트 작성: 3개 (test_ts_ratios, bench_short, bench_long)
- 벤치마크 반복: ~40회 (configs × runs)
- 서버 재기동: ~25회
- 검색/진단 쿼리: ~30회
**예상 token**: 매우 높음 (벤치 로그와 config 파일 반복 읽기)
---
**Report sealed: 2026-04-11**

View File

@@ -14,42 +14,60 @@
"fast": { "fast": {
"display_name": "Gemma 4 26B (Fast)", "display_name": "Gemma 4 26B (Fast)",
"model_path": "models/gemma-4-26B-A4B-it-Q4_K_M.gguf", "model_path": "models/gemma-4-26B-A4B-it-Q4_K_M.gguf",
"measured_tps": 74.65, "measured_tps": 72.04,
"args": [ "args": [
"-ngl", "999", "--mmproj",
"-c", "262144", "models/gemma-4-26B-mmproj-F16.gguf",
"-np", "1", "-ngl",
"-fa", "on", "999",
"--cache-type-k", "f16", "-c",
"--cache-type-v", "f16", "262144",
"-ub", "512", "-np",
"-b", "2048", "1",
"-t", "6", "-fa",
"-tb", "6", "on",
"--prio", "3", "--cache-type-k",
"--mlock", "q8_0",
"--poll", "50" "--cache-type-v",
"q8_0",
"-ub",
"512",
"-b",
"2048",
"-ts",
"0.43,0.57"
] ]
}, },
"balanced": { "balanced": {
"display_name": "Qwen 3.5 35B (Balanced)", "display_name": "Qwen 3.5 35B (Balanced)",
"model_path": "models/Qwen3.5-35B-A3B-Q4_K_M.gguf", "model_path": "models/Qwen3.5-35B-A3B-Q4_K_M.gguf",
"measured_tps": 61.62, "measured_tps": 64.16,
"args": [ "args": [
"-ngl", "999", "--mmproj",
"-c", "262144", "models/mmproj-F16.gguf",
"-np", "1", "--no-mmproj-offload",
"-fa", "on", "-ngl",
"--cache-type-k", "q4_0", "999",
"--cache-type-v", "q4_0", "-c",
"-ub", "128", "262144",
"-b", "512", "-np",
"-t", "6", "1",
"-tb", "6", "-fa",
"--prio", "3", "on",
"--mlock", "--cache-type-k",
"--poll", "50", "q4_0",
"-ts", "0.5,0.5" "--cache-type-v",
"q4_0",
"-ub",
"256",
"-b",
"512",
"-t",
"6",
"-tb",
"6",
"-ts",
"0.48,0.52"
] ]
}, },
"deep-coder": { "deep-coder": {
@@ -57,19 +75,31 @@
"model_path": "models/gemma-4-31B-it-Q4_K_M.gguf", "model_path": "models/gemma-4-31B-it-Q4_K_M.gguf",
"measured_tps": 16.0, "measured_tps": 16.0,
"args": [ "args": [
"-ngl", "999", "-ngl",
"-c", "196608", "999",
"-np", "1", "-c",
"-fa", "on", "196608",
"--cache-type-k", "q4_0", "-np",
"--cache-type-v", "q4_0", "1",
"-ub", "128", "-fa",
"-b", "512", "on",
"-t", "6", "--cache-type-k",
"-tb", "6", "q4_0",
"--prio", "3", "--cache-type-v",
"q4_0",
"-ub",
"128",
"-b",
"512",
"-t",
"6",
"-tb",
"6",
"--prio",
"3",
"--mlock", "--mlock",
"--poll", "50" "--poll",
"50"
] ]
}, },
"deep-logic": { "deep-logic": {
@@ -77,20 +107,33 @@
"model_path": "models/Qwen3.5-27B-Q4_K_M.gguf", "model_path": "models/Qwen3.5-27B-Q4_K_M.gguf",
"measured_tps": 16.7, "measured_tps": 16.7,
"args": [ "args": [
"-ngl", "999", "-ngl",
"-c", "262144", "999",
"-np", "1", "-c",
"-fa", "on", "262144",
"--cache-type-k", "q4_0", "-np",
"--cache-type-v", "q4_0", "1",
"-ub", "512", "-fa",
"-b", "1024", "on",
"-t", "6", "--cache-type-k",
"-tb", "6", "q4_0",
"--prio", "3", "--cache-type-v",
"q4_0",
"-ub",
"512",
"-b",
"1024",
"-t",
"6",
"-tb",
"6",
"--prio",
"3",
"--mlock", "--mlock",
"--poll", "50", "--poll",
"-ts", "0.5,0.5" "50",
"-ts",
"0.5,0.5"
] ]
}, },
"ultra": { "ultra": {
@@ -98,21 +141,36 @@
"model_path": "models/Q4_K_M/Qwen3.5-122B-A10B-Q4_K_M-00001-of-00003.gguf", "model_path": "models/Q4_K_M/Qwen3.5-122B-A10B-Q4_K_M-00001-of-00003.gguf",
"measured_tps": 8.95, "measured_tps": 8.95,
"args": [ "args": [
"-ngl", "999", "-ngl",
"-ncmoe", "48", "999",
"-c", "262144", "-ncmoe",
"-np", "1", "48",
"-fa", "on", "-c",
"--cache-type-k", "q4_0", "262144",
"--cache-type-v", "q4_0", "-np",
"-ub", "512", "1",
"-b", "2048", "-fa",
"-t", "8", "on",
"-tb", "8", "--cache-type-k",
"--prio", "3", "q4_0",
"--poll", "50", "--cache-type-v",
"--main-gpu", "1", "q4_0",
"-sm", "none", "-ub",
"512",
"-b",
"2048",
"-t",
"8",
"-tb",
"8",
"--prio",
"3",
"--poll",
"50",
"--main-gpu",
"1",
"-sm",
"none",
"--no-mmap" "--no-mmap"
] ]
} }

View File

@@ -0,0 +1,272 @@
# v3 — Balanced Role Retuning Log (Qwen 3.5 35B-A3B)
**Date:** 2026-04-11
**Target role:** `balanced` (Qwen3.5-35B-A3B Q4_K_M)
**Goal:** 기존 `measured_tps: 61.62` 기준을 재검증하고, 진짜 최적 구성을 실측 기반으로 확정
**Result:** 최종 TPS **64.16 t/s** (짧은 프롬프트) / **62.00 t/s** (3,100 토큰 긴 프롬프트)
**Status:** ✅ 확정
---
## 1. 재튜닝 동기
Phase 01 종료 후 `engine_models.json``balanced` 설정이 여러 이유로 일관되지 않게 수정되어 있었음:
- `--mmproj` 추가 (비전 지원용, 다른 작업자가 넣음)
- `--mlock --poll 50 --prio 3` 등 Phase 01 최종본과 불일치
- `-ts 0.5,0.5` (이중 GPU 분할) 상태에서 compute buffer OOM 발생
- 실측 속도가 레퍼런스(61.62) 대비 33~42 t/s 수준으로 떨어짐
재튜닝을 통해 **원인 규명** + **안정 + 최적 설정 확정**이 목표.
---
## 2. 하드웨어 진단 (핵심 발견)
| GPU | 모델 | 최대 PCIe | 현재 |
|-----|------|---------|------|
| 0 | RTX 3060 12GB | Gen3 x16 | **Gen3 x4** (슬롯 4레인) |
| 1 | RTX 3060 12GB | Gen4 x16 | **Gen4 x16** |
**핵심: GPU 0은 PCIe 3.0 × 4 슬롯에 있음 (3.94 GB/s).** GPU 1은 PCIe 4.0 × 16 (31.5 GB/s). GPU 0이 GPU 1의 1/8 대역폭.
이 비대칭이 모든 하이브리드/멀티-GPU 성능 문제의 근본 원인.
---
## 3. Qwen3.5-35B-A3B 아키텍처 실측 데이터
llama-server 로드 로그 기준:
```
architecture = qwen35moe
file size = 20.49 GiB (Q4_K_M, 5.08 BPW)
n_params = 34.66 B
n_layer = 40
n_head_kv = 2
n_embd_head_k = 256
n_embd_head_v = 256
n_expert = 256 (activated: 8)
full_attention_interval = 4
```
**중요:** 40 레이어 중 **full attention은 10개만** (매 4번째). 나머지 30개는 **Gated Delta Net (SSM/Mamba-like) 레이어**로 recurrent state 사용. KV 캐시는 10 레이어에만 발생.
### KV 캐시 실측
| 컨텍스트 | KV 캐시 (q4_0) |
|---------|---------------|
| 128K | 720 MiB |
| 256K | 1,440 MiB |
(초기 추정 5GB는 오류였음 — 40레이어 전부 attention이라고 오해)
---
## 4. `-ts` (Tensor Split) 비율 스윕 결과
자동화 스크립트(`scripts/test_ts_ratios.py`)로 여러 비율 테스트:
| ratio | status | PP | c0 model | c1 model | c0 compute | c1 compute |
|-------|--------|-----|---------|---------|-----|-----|
| 0.5,0.5 | ready | **Fallback** | 10,540 | 9,931 | 203 | 123 |
| **0.48,0.52** | ready | ✅ **ON** | 10,036 | 10,434 | 600 | 384 |
| 0.47,0.53 | ready | ✅ ON | 10,036 | 10,434 | 600 | 384 |
| 0.45,0.55 | error | — | — | — | — | — |
| 0.43,0.57 | error | — | — | — | — | — |
| 0.40,0.60 | error | — | — | — | — | — |
**해석:**
- 40 레이어 기준 llama.cpp가 ratio를 정수 레이어로 반올림: 0.48 & 0.47 둘 다 19/21 분할이라 동일한 메모리 배치
- 0.5,0.5 (20/20)에서는 CUDA0 compute buffer가 PP 모드 요구치(600 MiB)를 수용 못해 자동 Fallback
- 0.45,0.55 이상은 CUDA1이 22+ 레이어 적재로 OOM
- **결론: PP on 유일 비율은 0.48,0.52 (또는 동등한 0.47,0.53)**
---
## 5. `-ub` (Ubatch) 스윕 결과 — 핵심 발견
짧은 프롬프트만 테스트해서 처음에 `-ub` 효과를 놓쳤음. 긴 프롬프트(3,100 토큰)로 재측정:
| 설정 | PP | Prompt t/s | Gen t/s | 3,100 토큰 prefill | GPU0 여유 |
|------|-----|-----------|---------|-----------------|---------|
| ub 128 | ✅ ON | 649 | 62.01 | 4.85s | 216 MiB |
| **ub 256** | ❌ OFF | **1,157** | **62.00** | **2.68s** | **411 MiB** |
| ub 384 b 768 | ❌ OFF | 1,275 | 61.61 | 2.43s | 133 MiB |
**핵심 인사이트:**
1. **`-np 1` 단일 사용자 환경에서 PP는 실질 이득 없음** — Pipeline Parallelism은 다중 요청 배칭에 의미가 있음. 단일 시퀀스면 overlap 할 대상이 없음.
2. **PP off가 오히려 유리** — compute buffer 작아져서 `-ub` 더 올릴 수 있음 → prefill 속도 대폭 향상
3. **`-ub` 수익률 체감:**
- 128 → 256: **+78%** (649 → 1,157 t/s)
- 256 → 384: +10.2% (1,157 → 1,275 t/s)
- 안정성 대비 256이 스윗 스팟
4. **Gen 속도는 `-ub`와 무관** — 모두 62 t/s. Gen은 KV 캐시 크기 + PCIe x4 병목이 결정.
---
## 6. mmproj 처리 결정
Qwen3.5-35B-A3B는 natively 멀티모달이라 mmproj 필요. 하지만 GPU에 올리면:
```
VRAM 수지 (256K, -ts 0.48,0.52):
모델 가중치: 10,036 (GPU0) + 10,434 (GPU1)
KV 캐시: 720 + 720 = 1,440 MiB
Compute: ~600 + ~384 MiB
mmproj: 858 MiB ← 추가 부담 → OOM
```
**해법: `--no-mmproj-offload` 추가** → mmproj를 CPU RAM에 유지.
| 항목 | GPU 오프로드 | **CPU 오프로드** |
|------|------------|------------|
| VRAM 절약 | — | 858 MiB |
| 텍스트 추론 | 동일 | **동일 (손실 0)** |
| 이미지 인코딩 | GPU 빠름 | CPU 6.4초 (640×640 기준) |
**Hermes Agent 사용 패턴** = 95% 텍스트, 가끔 스크린샷 → **CPU 오프로드가 확실히 유리**.
### 이미지 토큰 계산식
```
patch_size = 16
n_merge = 2
→ tokens = (width/32) × (height/32)
```
| 해상도 | 토큰 |
|--------|------|
| 640×640 | 400 |
| 768×768 | 576 |
| 1024×1024 | 1,024 (권장 최소) |
| 2048×2048 | 4,096 (최대) |
---
## 7. 제거된 옵션 (실측 영향 없음 확인)
| 옵션 | 제거 이유 | Δ TPS |
|------|---------|-------|
| ~~`--mlock`~~ | 전용 추론기. 시스템 RAM 여유. mmap 페이지 잠금 불필요 | 0.04 |
| ~~`--poll 50`~~ | GPU polling. `-np 1` 환경에선 효과 없음 | 0.00 |
| ~~`--prio 3`~~ | 프로세스 우선순위. 전용기라 경쟁 없음 | 0.00 |
제거 후 속도: 64.16 t/s (유지)
---
## 8. 최종 확정 옵션
```json
{
"balanced": {
"display_name": "Qwen 3.5 35B (Balanced)",
"model_path": "models/Qwen3.5-35B-A3B-Q4_K_M.gguf",
"measured_tps": 64.16,
"args": [
"--mmproj", "models/mmproj-F16.gguf",
"--no-mmproj-offload",
"-ngl", "999",
"-c", "262144",
"-np", "1",
"-fa", "on",
"--cache-type-k", "q4_0",
"--cache-type-v", "q4_0",
"-ub", "256",
"-b", "512",
"-t", "6",
"-tb", "6",
"-ts", "0.48,0.52"
]
}
}
```
---
## 9. 최종 실측 성능
### 텍스트 추론
| 시나리오 | Prompt t/s | Gen t/s | VRAM 여유 |
|---------|-----------|---------|---------|
| 짧은 프롬프트 (170 tok) | — | **64.16** | GPU0 411 / GPU1 539 MiB |
| 긴 프롬프트 (3,100 tok) | **1,157** | **62.00** | 동일 |
### 비전 추론 (mmproj CPU)
| 단계 | 속도 / 시간 |
|------|------------|
| 이미지 인코딩 (CPU, 640×640) | 5.3초 (encode) + 1.0초 (decode) = **6.4초** |
| 이미지 이후 생성 | 62 t/s |
### VRAM 최종
```
GPU 0 11,900 MiB (여유 216 MiB) Gen3 x4 [PCIe 병목]
GPU 1 11,710 MiB (여유 401 MiB) Gen4 x16
합계 23,610 MiB 중 사용 │ 966 MiB 여유
```
### CPU RAM
```
llama-server Working Set: ~23 GB
├── mmap 모델 (lazy) 20.5 GB
├── mmproj (CPU 할당) 0.86 GB
├── CUDA_Host compute buffer 0.39 GB
├── CPU compute buffer 0.25 GB
└── 기타 ~0.08 GB
```
---
## 10. 알려진 구조적 제약
1. **GPU 0 PCIe 3.0 x4 슬롯 병목** — Gen 속도 62 t/s 상한의 주원인. 물리적 한계라 소프트웨어로 해결 불가.
2. **Pipeline Parallelism 자동 Fallback** — compute buffer가 `-ub 256` 시 CUDA0 한계 초과. 다만 `-np 1` 환경에선 실질 손실 없음.
3. **mmproj CPU 상주** — 이미지 인코딩 시 GPU 대비 ~3-5배 느림. 사용 빈도가 낮아 허용.
### 향후 개선 여지
- GPU 0을 PCIe 4.0 x16 슬롯으로 물리 이전 시 Gen 속도 추가 이득 기대 (~70+ t/s 가능성)
- 비전 사용이 잦아지면 `--no-mmproj-offload` 재고 필요
---
## 11. 레퍼런스 대비
```
Phase 01 측정치: 61.62 t/s
v3 확정치: 64.16 t/s (+4.1%)
```
Phase 01은 **단일 GPU 환경**에서 튜닝되었음 (`found 1 CUDA devices` 로그 확인). 현재는 **듀얼 GPU (비대칭 PCIe)** + mmproj 제약 + PP 동작을 모두 반영한 새로운 baseline.
---
## 12. 검증 절차 (재현용)
```bash
# 기동
run_variet_engine.bat
# 짧은 프롬프트 속도
curl -s http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"balanced","messages":[{"role":"user","content":"Write 200 words about computing history."}],"max_tokens":300}' \
| python -c "import json,sys; d=json.load(sys.stdin); print(d['timings']['predicted_per_second'])"
# 긴 프롬프트 속도
python scripts/bench_long.py "verify"
# VRAM 확인
nvidia-smi --query-gpu=index,memory.used,memory.free,pcie.link.gen.current,pcie.link.width.current --format=csv
```
---
**Document sealed: 2026-04-11**

View File

@@ -0,0 +1,223 @@
# v3 — Fast Role Retuning Log (Gemma 4 26B-A4B)
**Date:** 2026-04-11
**Target role:** `fast` (Gemma 4 26B-A4B Q4_K_M)
**Goal:** 레퍼런스 `measured_tps: 74.65` 재검증 및 vision 지원 추가
**Result:** Text 71.89 t/s + Vision GPU 인코딩 ~1초 + VRAM 여유 ~2 GB
**Status:** ✅ 확정
---
## 1. 재튜닝 동기
- 기존 `fast` 설정(f16 KV, `--prio/--mlock/--poll` 등)이 Phase 01 시점 단일 GPU 기준으로 튜닝됐음
- 듀얼 GPU 환경 + 비전 지원 추가 필요
- `balanced` 재튜닝과 동일한 기준으로 재검증
---
## 2. Gemma 4 26B-A4B 아키텍처 실측 데이터
```
architecture: gemma4
file size: 15.63 GiB (Q4_K_M, 5.32 BPW)
n_layer: 30
n_head_kv: [8,8,8,8,8,2, 8,8,8,8,8,2, 8,8,8,8,8,2, 8,8,8,8,8,2, 8,8,8,8,8,2]
n_expert: 128 (8 active)
n_ctx_train: 262144 (256K native)
```
**핵심 구조:** 매 6번째 레이어만 full attention (`n_head_kv=2`), 나머지 25개는 **SWA (Sliding Window Attention)**. KV 캐시는 **5 full attention 레이어만** 큰 공간 차지.
### KV 캐시 실측 비교
| 컨텍스트 | f16 | q8_0 | q4_0 |
|---------|-----|------|------|
| Full attention (5 layers) | 5,120 MiB | 2,720 MiB | ~1,360 MiB |
| SWA (25 layers, 1,536 cells) | 300 MiB | 159 MiB | ~80 MiB |
| 총계 | **5,420 MiB** | **2,879 MiB** | **~1,440 MiB** |
---
## 3. VRAM 배치 (`-ts 0.43,0.57`)
13/17 레이어 분할:
```
Model: CUDA0 7,146 / CUDA1 8,857 MiB
KV q8_0: CUDA0 1,088 / CUDA1 1,632 (FA) + CUDA0 ~70 / CUDA1 ~90 (SWA)
mmproj: 1,138 MiB (CUDA0, --mmproj-offload 기본)
Compute: CUDA0 ~820 / CUDA1 ~528 (PP fallback)
Overhead: ~300 MiB per GPU
합계: GPU0 10,924 MiB (여유 1,192) / GPU1 11,230 MiB (여유 881)
```
---
## 4. `-ts` 비율 스윕 결과
| `-ts` | 분할 | CUDA0 model | CUDA1 model | 결과 |
|-------|-----|-----------|-----------|-----|
| 0.5,0.5 | 15/15 | 8,177 | 7,826 | mmproj OOM |
| 0.47,0.53 | 14/16 | 7,708 | 8,295 | mmproj OOM |
| **0.43,0.57** | **13/17** | **7,146** | **8,857** | ✅ 최적 |
| 0.4,0.6 | 12/18 | 6,676 | 9,327 | vision decode crash (CUDA1 OOM) |
**0.43,0.57이 mmproj + 양쪽 GPU 여유의 스윗 스팟.**
---
## 5. `-ub` / `-b` 스윕 결과
짧은 프롬프트(170 tok) vs 긴 프롬프트(3,100 tok) 양쪽 측정:
| 설정 | Prompt 읽기 | Gen 쓰기 | VRAM 여유 (G0/G1) |
|------|-----------|---------|-----------------|
| ub 128 | — | 74.95 (`-ts 0.5`) | — |
| ub 256 | 1,429 | 67 | 411/539 |
| **ub 512** (확정) | **1,672** | **66.67** | **1,192/881** |
| ub 768 | 1,695 | 65.94 | 761/603 |
| ub 1024 | — | — | init OOM |
| ub 2048 | — | — | init OOM |
**`-ub 512`가 스윗 스팟.** 더 크게 해도 이득 마진 작음 (1.3%) + VRAM 위험 증가.
---
## 6. KV 캐시 양자화 비교
| KV | Text (short) | Text (long gen) | VRAM 여유 |
|----|-------------|----------------|---------|
| f16 (초기) | 73.43 t/s | — | 298/427 |
| f16 + mmproj CPU | 73.67 | — | 266/427 |
| q8_0 | 73.20 | — | 1,321/1,925 |
| **q8_0 + mmproj GPU** | **71.89** | **66.67** | **1,192/881** |
**q8_0 전환으로 VRAM 2.5 GB 추가 확보 → mmproj GPU 장착 가능.** 품질 손실 미미.
---
## 7. Vision 인코딩 (mmproj GPU)
640×640 JPEG 고양이 이미지, 3회 반복:
| 단계 | 시간 | 비고 |
|------|-----|-----|
| 이미지 인코딩 (GPU) | ~1,000 ms | 283 토큰, 280 t/s |
| 생성 후속 | 72 t/s | Text와 동일 |
| **per-image 총 시간** | **~2.4초** | 1s encode + 1.4s gen |
**CPU 오프로드 대비 ~12배 빠름** (Config 2 mmproj CPU: 12.4초). GPU 장착 가치 있음.
---
## 8. Speculative Decoding 실험 (채택 안 함)
Draft 모델 `gemma-4-E2B-it-Q4_K_M.gguf` 사용한 추측 디코딩 실험:
| 항목 | 결과 |
|------|-----|
| Gen 평균 (5회) | 75.68 t/s (Run 1 49.68 포함) |
| Gen BEST | 86.18 t/s (+18%) |
| Gen Run 2-5 평균 | 82.18 (+14%) |
| Cold start (Run 1) | **49.68 t/s (-31% vs baseline)** |
| Acceptance rate | 82% (일반 텍스트) |
| Tokenizer | 불일치 경고 |
| mmproj 호환 | ❌ 추가 튜닝 필요 |
**결론: 채택 안 함.** 이유:
1. +14% 이득 vs 복잡성 폭증 (flag +5개, draft 모델 관리)
2. **Cold start -31% 페널티** — idle 후 첫 응답 느림 (Hermes Agent 사용 패턴 상극)
3. mmproj 추가하려면 draft 제거해야 함
4. tokenizer 불일치 오버헤드 상시 발생
5. acceptance rate 워크로드 의존 (한국어/코드는 더 낮을 것)
---
## 9. 제거된 옵션
| 옵션 | 제거 이유 |
|------|---------|
| ~~`--prio 3`~~ | 전용기, 경쟁 없음 |
| ~~`--mlock`~~ | mmap 페이지 잠금 불필요 |
| ~~`--poll 50`~~ | `-np 1` 단일 요청 환경 |
| ~~`-t 6 / -tb 6`~~ | `-ngl 999` 풀 GPU 오프로드라 무의미 |
balanced 재튜닝과 동일한 검증 거침. Text 속도 영향 오차 범위.
---
## 10. 최종 확정 옵션
```json
{
"fast": {
"display_name": "Gemma 4 26B (Fast)",
"model_path": "models/gemma-4-26B-A4B-it-Q4_K_M.gguf",
"measured_tps": 72.04,
"args": [
"--mmproj", "models/gemma-4-26B-mmproj-F16.gguf",
"-ngl", "999",
"-c", "262144",
"-np", "1",
"-fa", "on",
"--cache-type-k", "q8_0",
"--cache-type-v", "q8_0",
"-ub", "512",
"-b", "2048",
"-ts", "0.43,0.57"
]
}
}
```
---
## 11. 최종 실측 성능
### Text 추론
| 시나리오 | Prompt t/s | Gen t/s |
|---------|-----------|---------|
| 짧은 프롬프트 (170 tok, 5회) | — | **71.89** (BEST 72.91) |
| 긴 프롬프트 (3,100 tok, 3회) | **1,672** | **66.67** |
### Vision 추론 (GPU mmproj)
| 단계 | 시간/속도 |
|------|---------|
| 이미지 인코딩 (640×640) | ~1,000 ms |
| 이미지 토큰 수 | 283 |
| 생성 속도 | 72 t/s |
### VRAM 최종
```
GPU 0 10,924 MiB (여유 1,192 MiB) Gen3 x4
GPU 1 11,230 MiB (여유 881 MiB) Gen4 x16
```
---
## 12. Phase 01 대비
| 항목 | Phase 01 | v3 재튜닝 |
|------|---------|----------|
| 측정치 | 74.65 t/s | 71.89 t/s |
| 차이 | — | **-2.76 (-3.7%)** |
| GPU 구성 | 단일 | 듀얼 (비대칭 PCIe) |
| mmproj | 없음 | **GPU 지원** |
| KV | f16 | q8_0 |
| VRAM 여유 | — | 2,073 MiB |
**약 3.7% 속도 손실 대신 Vision 기능 확보**. 비대칭 PCIe (GPU0 x4) 구조에서 듀얼 GPU로 돌리는 비용. 단일 GPU로 돌아가려면 `--cpu-moe` + `-ngl` 조정 필요하지만 현재 검증 안 됨.
---
## 13. Speculative Decoding 관련 정리
E2B 드래프트 모델(`gemma-4-E2B-it-Q4_K_M.gguf`, 2.9 GB) 및 mmproj(`gemma-4-E2B-mmproj-F16.gguf`, 940 MB) 파일은 **최종 채택 안 됨**. 세션 중 실험 후 삭제 권장 (총 3.8 GB 디스크 회수).
---
**Document sealed: 2026-04-11**

67
scripts/bench_long.py Normal file
View File

@@ -0,0 +1,67 @@
"""Benchmark with long prompts to measure prompt processing (prefill) speed."""
import json
import time
import urllib.request
import sys
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
BASE_SENTENCE = (
"The history of computing is a vast and multifaceted journey that spans millennia, "
"from the earliest mechanical calculating aids to the sophisticated digital systems of today. "
"It begins with simple counting devices like the abacus, which originated in ancient Mesopotamia "
"around 2300 BCE and was later refined by Chinese and Roman civilizations. "
"These early tools laid the conceptual groundwork for mechanical computation. "
)
def make_prompt(seed):
# each seed produces a slightly different long prompt to defeat caching
unique = f"Session {seed}. Random seed value: {seed * 31337 + 17}. "
long_text = unique + (BASE_SENTENCE * 40)
return (
"Read the following text carefully, then answer in exactly one short sentence:\n\n"
f"{long_text}\n\n"
"Question: What is the main subject of the text above? Answer in one short sentence only."
)
def bench(label, seed, gen_tokens=150):
payload = {
"model": "balanced",
"messages": [{"role": "user", "content": make_prompt(seed)}],
"max_tokens": gen_tokens,
"stream": False,
"temperature": 0.3,
}
req = urllib.request.Request(
"http://localhost:8000/v1/chat/completions",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
t0 = time.time()
with urllib.request.urlopen(req, timeout=600) as r:
d = json.loads(r.read())
total = time.time() - t0
t = d.get("timings", {})
print(f"[{label}]")
print(f" prompt: {t['prompt_n']:>5} tok {t['prompt_ms']:>7.0f} ms {t['prompt_per_second']:>7.2f} t/s")
print(f" gen: {t['predicted_n']:>5} tok {t['predicted_ms']:>7.0f} ms {t['predicted_per_second']:>7.2f} t/s")
print(f" total: {total:.2f} s")
return t
if __name__ == "__main__":
label = sys.argv[1] if len(sys.argv) > 1 else "run"
results = []
for i in range(3):
t = bench(f"{label} #{i+1}", seed=i + 1)
results.append(t)
print()
if results:
avg_prompt = sum(r["prompt_per_second"] for r in results) / len(results)
avg_gen = sum(r["predicted_per_second"] for r in results) / len(results)
print(f"=== [{label}] AVG === prompt: {avg_prompt:.2f} t/s | gen: {avg_gen:.2f} t/s")

87
scripts/bench_short.py Normal file
View File

@@ -0,0 +1,87 @@
"""Phase 01 style short-prompt benchmark using llama.cpp internal timings."""
import json
import urllib.request
import sys
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
def bench_text(model_name, n=200):
payload = json.dumps({
"model": model_name,
"messages": [{"role": "user", "content": "Count from 1 to 50, each number on a new line."}],
"max_tokens": n,
"temperature": 0,
}).encode()
req = urllib.request.Request(
"http://127.0.0.1:8000/v1/chat/completions",
data=payload,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as r:
return json.loads(r.read()).get("timings", {})
def bench_image(model_name, image_path, prompt):
import base64
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
payload = json.dumps({
"model": model_name,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
],
}],
"max_tokens": 100,
"temperature": 0.3,
}).encode()
req = urllib.request.Request(
"http://127.0.0.1:8000/v1/chat/completions",
data=payload,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=600) as r:
return json.loads(r.read()).get("timings", {})
def main():
label = sys.argv[1] if len(sys.argv) > 1 else "run"
model = sys.argv[2] if len(sys.argv) > 2 else "fast"
do_image = "--image" in sys.argv
print(f"=== [{label}] model={model} do_image={do_image} ===")
print("warmup...")
try:
bench_text(model, 10)
except Exception as e:
print(f"warmup err: {e}")
print("text 5-run:")
runs = []
for i in range(5):
t = bench_text(model, 200)
runs.append(t["predicted_per_second"])
print(f" Run {i+1}: gen {t['predicted_per_second']:.2f} t/s ({t['predicted_n']} tok, {t['predicted_ms']:.0f}ms) | prompt {t['prompt_per_second']:.1f} t/s ({t['prompt_n']} tok)")
avg = sum(runs) / len(runs)
print(f" TEXT AVG: {avg:.2f} t/s BEST: {max(runs):.2f} MIN: {min(runs):.2f}")
if do_image:
prompts = [
"What do you see in this image? One sentence.",
"Describe the subject and background in one sentence.",
"What is the most prominent feature? One sentence.",
]
print("vision 3-run (640x640 cat):")
for i, p in enumerate(prompts):
t = bench_image(model, "logs/vision_test/sample.jpg", p)
print(f" Run {i+1}: prompt {t['prompt_n']} tok ({t['prompt_ms']:.0f}ms, {t['prompt_per_second']:.1f} t/s) | gen {t['predicted_n']} tok ({t['predicted_per_second']:.1f} t/s)")
if __name__ == "__main__":
main()

145
scripts/test_ts_ratios.py Normal file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python
"""Test multiple -ts ratios to find which ones start normally (no OOM, PP enabled)."""
import subprocess
import time
import json
import urllib.request
import urllib.error
import sys
import re
from pathlib import Path
ROOT = Path(__file__).parent.parent
CONFIG_FILE = ROOT / "config" / "engine_models.json"
LLAMA_LOG = ROOT / "logs" / "llama-server.log"
ENGINE_LOG = ROOT / "logs" / "engine_test.log"
PYTHON = r"C:\ProgramData\miniforge3\envs\variet-llm\python.exe"
ENGINE_SCRIPT = ROOT / "engine" / "variet_engine.py"
RATIOS = [
("0.5", "0.5"),
("0.48", "0.52"),
("0.47", "0.53"),
("0.45", "0.55"),
("0.43", "0.57"),
("0.40", "0.60"),
]
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
def kill_servers():
subprocess.run(
["powershell", "-Command",
"Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -like '*engine/variet_engine.py*' -or $_.Name -eq 'llama-server.exe' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"],
capture_output=True
)
time.sleep(2)
def update_config(ts_a, ts_b):
with open(CONFIG_FILE, encoding="utf-8") as f:
cfg = json.load(f)
args = cfg["roles"]["balanced"]["args"]
for i, a in enumerate(args):
if a == "-ts" and i + 1 < len(args):
args[i + 1] = f"{ts_a},{ts_b}"
break
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
def start_engine():
LLAMA_LOG.write_text("")
ENGINE_LOG.write_text("")
return subprocess.Popen(
[PYTHON, str(ENGINE_SCRIPT)],
cwd=str(ROOT),
stdout=open(ENGINE_LOG, "wb"),
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
)
def wait_for_result(timeout=180):
"""Return (status, log_tail) where status is 'ready'|'oom'|'error'|'timeout'."""
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(3)
# check engine status
try:
with urllib.request.urlopen("http://localhost:8000/engine/status", timeout=2) as r:
data = json.loads(r.read())
if data.get("state") == "ready":
return "ready", ""
if data.get("state") == "error":
return "error", ""
except Exception:
pass
return "timeout", ""
def analyze_log():
if not LLAMA_LOG.exists():
return {}
text = LLAMA_LOG.read_text(encoding="utf-8", errors="ignore")
result = {
"pp_enabled": "pipeline parallelism enabled" in text,
"pp_fallback": "retrying without pipeline parallelism" in text,
"oom": "out of memory" in text,
"listening": "main: server is listening" in text,
}
m = re.search(r"CUDA0 model buffer size = +([0-9.]+) MiB", text)
if m:
result["cuda0_model"] = float(m.group(1))
m = re.search(r"CUDA1 model buffer size = +([0-9.]+) MiB", text)
if m:
result["cuda1_model"] = float(m.group(1))
m = re.search(r"CUDA0 KV buffer size = +([0-9.]+) MiB", text)
if m:
result["cuda0_kv"] = float(m.group(1))
m = re.search(r"CUDA1 KV buffer size = +([0-9.]+) MiB", text)
if m:
result["cuda1_kv"] = float(m.group(1))
m = re.search(r"CUDA0 compute buffer size = +([0-9.]+) MiB", text)
if m:
result["cuda0_compute"] = float(m.group(1))
m = re.search(r"CUDA1 compute buffer size = +([0-9.]+) MiB", text)
if m:
result["cuda1_compute"] = float(m.group(1))
return result
def main():
results = []
print(f"{'ratio':<14} {'status':<10} {'PP':<6} {'cuda0_m':<9} {'cuda1_m':<9} {'cuda0_kv':<9} {'cuda1_kv':<9} {'c0_comp':<9} {'c1_comp':<9}")
print("-" * 110)
for ts_a, ts_b in RATIOS:
label = f"{ts_a},{ts_b}"
kill_servers()
update_config(ts_a, ts_b)
proc = start_engine()
status, _ = wait_for_result(timeout=180)
info = analyze_log()
pp = "ON" if info.get("pp_enabled") and not info.get("pp_fallback") else ("FALLBACK" if info.get("pp_fallback") else "?")
c0m = f"{info.get('cuda0_model', 0):.0f}"
c1m = f"{info.get('cuda1_model', 0):.0f}"
c0kv = f"{info.get('cuda0_kv', 0):.0f}"
c1kv = f"{info.get('cuda1_kv', 0):.0f}"
c0c = f"{info.get('cuda0_compute', 0):.0f}"
c1c = f"{info.get('cuda1_compute', 0):.0f}"
print(f"{label:<14} {status:<10} {pp:<6} {c0m:<9} {c1m:<9} {c0kv:<9} {c1kv:<9} {c0c:<9} {c1c:<9}")
results.append({"ratio": label, "status": status, "info": info})
proc.terminate()
time.sleep(1)
kill_servers()
print("\nDone.")
if __name__ == "__main__":
main()