fix(glm53): fp8 dense 사본을 적재가 끝난 뒤에 뜬다 — 이른 사본이 그대로 서빙됐다 - #98
Conversation
#94 로 기본 무장한 첫 부팅에서 타깃이 드래프트 토큰 51,786 개 중 0 개를 수용했다. 디코드 8.8 tok/s (대조 ~32), Mean acceptance length 1.00. 드래프트 생성은 정상 (7,398 라운드 x 7 토큰) 이므로 망가진 것은 검증하는 쪽이다. 부팅 로그가 시점을 그대로 보여준다: 05:39:32 가중치 적재 시작 (294.50 초) 05:40:02 [fp8-dense] 180 linears quantized (3.67 GB) <- 적재 30 초 시점 05:43:59 [fp8-dense] 0 quantized, 180 kept bf16 05:44:26 [fp8-dense] 0 quantized, 180 kept bf16 05:44:26 Loading weights took 294.50 seconds 훅이 Glm5NextModel.load_weights 끝에 있었다. 그건 자식 훅이고 AutoWeightsLoader 는 체크포인트를 다 훑기 전에 그 안으로 들어올 수 있다. 그리고 사본은 스냅샷이다 -- quant_method 를 갈아끼우는 순간 apply() 는 q/ws 만 읽고 bf16 텐서는 다시는 안 읽힌다. 그래서 30 초 시점의 사본이 부팅 내내 서빙됐고, 뒤의 두 호출이 "0 quantized" 인 것은 이미 갈아끼워져 UnquantizedLinearMethod 가 아니었기 때문이다. 기본 off 였을 때는 아무도 이 경로를 밟지 않았다. - 훅을 Glm5NextForConditionalGeneration.load_weights 의 loader.load_weights() 반환 뒤로 옮긴다. 최상위이고 한 번만 불린다. - maybe_build_fp8_dense 를 재무장 가능하게: 이미 설치된 Fp8DenseMethod 는 건너뛰지 않고 ._base 로 풀어 오늘의 가중치로 다시 만든다. 이르게 부른 호출자가 낡은 사본을 남길 수 없다 -- 마지막 호출이 이긴다. - 사본 검사: 무장 직전에 랜덤 입력으로 fp8 경로와 bf16 경로를 맞춰 본다. 블록-fp8 은 bf16 에서 몇 % 안쪽이고 낡은 사본은 아예 다른 행렬이라 자릿수로 빗나가므로, 문턱(0.25)은 빡빡할 필요가 없다. 빗나가면 그 층만 bf16 으로 되돌리고 이름을 로그에 남긴다. - 프로브를 못 돌린 경우(None)는 무장을 유지한다. 실행이 안 됐다는 이유로 무장을 거부하면 실측된 이득을 미측정 걱정과 바꾸는 셈이다. 되돌리는 것은 실제로 빗나간 False 뿐이다. - 지문에 "N disarmed by the copy check" 를 더한다. #95 의 드래프터 호출부(dflash_utils)도 같은 함수를 쓰므로 검사가 함께 걸린다. tests/test_logic.py 669 검사 통과, compose-overlays.sh glm53 완주(14/12). 플릿 미검증 -- 부팅 필요. 판정 전제는 지문의 disarmed=0 과 수용률 회복이다.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_2f2ae752-2616-41be-9f00-3addf5e6402b) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Not approved: Cursor Bugbot was present but did not complete successfully (check skipped — usage limit), so there is no clean automated-review signal. Human review is needed; no reviewers were assigned because this repository has no assignable reviewer other than the PR author.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31894c9f71
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if isinstance(base, Fp8DenseMethod): | ||
| base = base._base |
There was a problem hiding this comment.
Restore the base method when re-arming fails
When this is the final re-arm after an earlier call installed a stale copy, these lines unwrap only the local base variable while mod.quant_method still references the old Fp8DenseMethod. If _quantize_fp8_block_padded or another operation in the subsequent try raises, the exception handler merely logs that the layer “stayed bf16,” leaving the stale copy active and allowing the same corrupted output this change is intended to prevent. Restore mod.quant_method = base on the failure path or before attempting the rebuild.
Useful? React with 👍 / 👎.
_copy_matches_source 가 method.apply() 를 호출하면, apply 의 자체 try/except 가 (1) 예외를 삼키고 (2) 그 자리에서 layer.quant_method 를 폴백으로 교체하며(빌드 중 상태 변이!) (3) 폴백의 출력을 반환한다 — 그러면 ref 도 폴백과 같은 경로라 오차 0, 'True' 판정. 깨진 W4 커널도 '무장 성공'으로 거짓 지문이 찍히고, 그 층은 매 호출 예외→fp8 폴백, W4 텐서는 순수 낭비. 이 마스킹은 main 의 fp8 경로(#98 도입)에도 동일하게 존재했다. 수정: 체크에 got_fn(직접 GEMM 호출)을 전달 — 깨진 커널은 예외(→None, fp8 은 종전 철학대로 무장) 또는 가비지(→False, W4 의 is-True 게이트는 기각)로 드러난다. 빌드 중 상태 변이도 사라짐. 덤: 모듈 문서의 무장 행을 스킴 노브(0|1|w4a8)로 갱신.
… 사다리 (#106) * perf(glm53): dense W4A8 — fp8_fp4_gemm_nt 로 가중치 한 단계 더, 액티베이션은 fp8 유지 스킴 노브: VLLM_GLM53_FP8_DENSE = 0 | 1(W8A8) | w4a8. W4 팔은: - 가중치: per_token_cast_to_fp4 (e2m1, 행별 128-블록 ue8m0 스케일, 함수가 K 패딩을 스스로 처리 — #92 의 행 패딩/출력 슬라이스 불필요) - 액티베이션: 기존 fp8 per-token-group quant 그대로 — 문헌이 정당화하는 축 (QServe: W4A4 는 20-25% 손실, W4A8 이 타협점) - GEMM: fp8_fp4_gemm_nt — MoE 전문가 178GB 가 타는 sm120_fp8_fp4_gemm_1d1d 패밀리의 dense 형태. vLLM 래퍼는 노출 안 하지만 vendored deep_gemm 에 nn/nt/tn/tt 전부 존재 안전 사다리 (전부 빌드 시점, 캡처 전): W4 패킹 → 프로브 GEMM(실제 커널 2행 시험) + 완화 copy check(4×_STALE_RTOL) → 실패 시 W8A8 쌍 → 그마저 실패 시 bf16. 재무장 언랩은 #98 로직 보존 (Fp8DenseMethod·W4A8DenseMethod 둘 다 언랩). 최악의 부팅 = 기본 팔과 동일. 기대: dense 읽기 1.93→0.97GB, 이상 −4.3ms, 1단계 실현율(76%) 준용 −3.3~−4.5ms ≈ +5.4~7.4% step/s. 게이트: 9/9, 한글 0/16, pos-1 ±2%p — W8A8 게이트 통과가 선행 조건이었으나 프로브/폴백 설계로 단독 팔로도 안전하므로 실험 팔로 게이트를 직접 받는다. * fix(w4a8): 리뷰 4건 — 죽은 코드, 이중 스케일 형식 순회, 관대한 카피체크, 메모리 문서화 - 죽은 _w4_dense_gemm_call/_flag 제거 - 스케일 형식을 packed-ue8m0 한 가지만 시도하던 것을 packed→plain 순회로: 커널의 C++ 체크를 못 읽으니 값 검증이 통과하는 쪽을 찾게 한다 — W4 팔이 실제로 무장할 확률을 높임 - _w4_probe_ok 제거·통합: _copy_matches_source 가 실GEMM 값비교라 프로브를 완전히 대체. 실험 스킴은 값 체크가 '실제로 실행되고 통과'(True)일 때만 무장 — fp8 경로의 '실패 안 함'보다 한 단계 엄격 - 카피체크 허용오차 4×(1.0)→2×(0.5): e2m1 행블록 실측 오차 0.02-0.08, 비상관 가비지 ~√2=1.41 — 1.0은 반쯤 가비지(faulty scale layout)가 새어들 수 있는 폭이었다 - W4A8DenseMethod 문서에 삼중 상주(bf16+fp8폴백+fp4, 랭크당 ~+1GB)와 런타임 폴백이 W8A8로 한 단계 떨어진다는 것 명시 * fix(fp8-dense): 카피체크가 메서드 내부 폴백에 마스킹되던 버그 — 2차 리뷰 발견 _copy_matches_source 가 method.apply() 를 호출하면, apply 의 자체 try/except 가 (1) 예외를 삼키고 (2) 그 자리에서 layer.quant_method 를 폴백으로 교체하며(빌드 중 상태 변이!) (3) 폴백의 출력을 반환한다 — 그러면 ref 도 폴백과 같은 경로라 오차 0, 'True' 판정. 깨진 W4 커널도 '무장 성공'으로 거짓 지문이 찍히고, 그 층은 매 호출 예외→fp8 폴백, W4 텐서는 순수 낭비. 이 마스킹은 main 의 fp8 경로(#98 도입)에도 동일하게 존재했다. 수정: 체크에 got_fn(직접 GEMM 호출)을 전달 — 깨진 커널은 예외(→None, fp8 은 종전 철학대로 무장) 또는 가비지(→False, W4 의 is-True 게이트는 기각)로 드러난다. 빌드 중 상태 변이도 사라짐. 덤: 모듈 문서의 무장 행을 스킴 노브(0|1|w4a8)로 갱신.


Summary
#94 로 기본 무장한 첫 부팅에서 타깃이 드래프트 토큰 51,786 개 중 0 개를 수용했다. 디코드 8.8 tok/s(대조 ~32),
Mean acceptance length 1.00. 드래프트 생성은 정상(7,398 라운드 × 7 토큰)이므로 망가진 것은 검증하는 쪽이다.부팅 로그가 시점을 그대로 보여준다:
훅이
Glm5NextModel.load_weights끝에 있었다. 그건 자식 훅이고AutoWeightsLoader는 체크포인트를 다 훑기 전에 그 안으로 들어올 수 있다. 그리고 사본은 스냅샷이다 —quant_method를 갈아끼우는 순간apply()는q/ws만 읽고 bf16 텐서는 다시는 안 읽힌다. 30 초 시점의 사본이 부팅 내내 서빙됐고, 뒤의 두 호출이 "0 quantized" 인 것은 이미 갈아끼워져UnquantizedLinearMethod가 아니었기 때문이다. 기본 off 였을 때는 아무도 이 경로를 밟지 않았다.Changes
Glm5NextForConditionalGeneration.load_weights의loader.load_weights()반환 뒤로 이동 — 최상위이고 한 번만 불린다Fp8DenseMethod를 건너뛰지 않고._base로 풀어 오늘의 가중치로 다시 만든다. 이르게 부른 호출자가 낡은 사본을 남길 수 없고 마지막 호출이 이긴다0.25)은 빡빡할 필요가 없다. 빗나가면 그 층만 bf16 으로 되돌리고 이름을 로그에 남긴다False뿐이다N disarmed by the copy check추가#95 의 드래프터 호출부(
dflash_utils)도 같은 함수를 쓰므로 검사가 함께 걸린다.왜 검사까지 넣나
시점만 고치면 이 사례는 막지만 부류는 안 막는다. "사본을 떴는데 원본이 나중에 바뀐다"는 이 레포가 반복해 겪는 조용한 결함이고, 이번엔
self-test PASS같은 기존 게이트를 전부 통과한 채 서빙까지 갔다. 검사는 원본과 사본이 어긋나는 순간을 부팅 로그에서 잡는다.Verification
tests/test_logic.py669 검사 통과launchers/compose-overlays.sh glm53완주 (14 overlays / 12 modules)py_compile통과disarmed by the copy check0🤖 Generated with Claude Code
Note
Medium Risk
Changes when dense fp8 quantization arms and what gets served at inference; mistakes would affect decode quality and speculative acceptance, but the move and per-layer probe reduce stale-weight risk.
Overview
Fixes a boot where fp8 dense snapshots were taken ~30s into a ~295s load and then served for the whole run, breaking target verification (0/51k draft accepts).
maybe_build_fp8_denseis no longer invoked fromGlm5NextModel.load_weights; it runs once afterAutoWeightsLoader.load_weightsreturns onGlm5NextForCausalLM, when checkpoint weights are settled.maybe_build_fp8_denseis now safe to call repeatedly: existingFp8DenseMethodlayers are unwrapped to_baseand re-quantized from current bf16 weights so an early caller cannot leave a stale copy. Before swappingquant_method,_copy_matches_sourcecompares fp8 vs bf16 on a random probe (relative error ≤0.25); only a definite mismatch disarms that layer (probe failure keeps fp8 armed). Boot logs report layers disarmed by the copy check.The same function is used for the DFlash2 drafter (
VLLM_DFLASH2_FP8_DENSE), so re-arm and copy-check apply there too.Reviewed by Cursor Bugbot for commit 31894c9. Bugbot is set up for automated code reviews on this repo. Configure here.