fix(serve/coli): make GPU-vs-fallback counters and chat status actually visible - #829
Conversation
- The splash strapline was hardcoded ("GLM-5.2 Β· 744B MoE Β· int4 Β· streaming
CPU") regardless of the loaded model or backend. It is now derived from the
model dir's config.json (name + expert count) and the requested backend
(COLI_METAL/COLI_CUDA/COLI_VULKAN); the old text remains only as fallback
for model-less commands. The engine's own [METAL]/[CUDA] line stays the
confirmation that the tier actually engaged.
- The stderr drain called p.stderr.read(), which only returns at EOF; the
engine stays alive on stdin, so every load-time status line was invisible
(the bounded 1s wait always expired against an empty file). Now reads
readline() in a loop with per-line write+flush.
- The "ready in Xs" detector scanned stderr for "loaded in ...", but that
line goes to stdout and was being discarded by stream_turn. Now captured
from the preamble and matched there.
- Added [METAL]/[CUDA] to the chat status-line whitelist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
β¦sed attention With an i4 snapshot (int4 weights, per-group scales = fmt=4, e.g. GLM-5.2-i4) the Metal backend never executed any decode work: moe_submit gated fmt != 1/2/6 and both fused-attention gates required kv_b.fmt==2 exactly, so every routed-expert block and every attention layer silently fell back to CPU. The [METAL] banner printed regardless, making the idle GPU hard to see. Routed experts: - moe_gemv gains an fmt=4 branch (per-group scale folded into the MAC, mirroring mm_gemv's; gsz at buffer index 10). - moe_submit / coli_metal_moe_block[_begin] take a gs4 parameter; the fmt allowlist now includes 4 (even positive group size required, else CPU). - colibri.c's MB_BUILD captures the experts' group size and poisons it on any mismatch (gs4=-1 -> moe_submit refuses -> whole subset stays on the CPU path, never wrong results). Fused attention: - a_deqrow (shared by a_qabs/a_ctx) now decodes per-row (fmt=2) or per-group (fmt=4) kv_b scales; kvb_gs threaded through AttnW and coli_metal_attn_decode / coli_metal_layer_decode, validated in encode_attention. Both colibri.c gates relaxed to kv_b.fmt 2 or 4. Observability: - run_serve now calls profile_print at exit under PROF=1: the cumulative METAL:/METAL-ATTN: counters were structurally unreachable in serve mode (only the oracle/generate exit paths printed them), so a served session could never show GPU-vs-fallback truth. stdin has hit EOF by then, so the frames cannot interleave with protocol a client is parsing. Tests (make metal-test, all green): - run_moe_g4: two batched-MoE fmt=4 cases vs a per-group CPU reference. - run_attn(kvb_g4=1): three fused-attention cases with grouped kv_b. - test-local FP8 helper renamed ref_fp8_nblk: quant.h:483 gained fp8_nblk returning int64_t, colliding with the test's self-contained int version (C++ cannot overload on return type), which broke the metal-test build. Measured on an M4 / 34 GB with GLM-5.2-i4 (391 GB, NGEN=8, RAM_GB=24): before: METAL blocks all-CPU, decode p50 11.4 s/forward; after: blocchi GPU 1248 | fallback CPU 0, METAL-ATTN layer GPU 546 (100% of decode layers), p50 5.9 s/forward with PIPE=1 DIRECT=1 (remaining time is expert I/O on this RAM-constrained host, not compute). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Welcome, and thank you β this is a real fix for a real defect, and you found the same root cause I did when I went through #813 independently: - if (!g_dev || (fmt != 1 && fmt != 2 && fmt != 6)) return nil;
+ if (!g_dev || (fmt != 1 && fmt != 2 && fmt != 4 && fmt != 6)) return nil;I have to tell you something before you invest more in it, and I would rather you hear it from me now than from a merge conflict later. #587 is doing the same work, and has been for two weeks. @RDouglasSharp opened it against #585 on 21 July. It touches the same four files this does β That is not your fault. Nothing on #813 or #585 pointed at #587 as work-in-progress, and the only reason I know is that I compared the file lists. The gap is mine to close, and I have asked myself the same question about DeepSeek twice this week. Your PR is not redundant, though, and this is the part worth keeping. #587 fixes the decode path. Yours also fixes why nobody noticed for two weeks:
That is the actual bug behind #813. The banner said "GPU-enabled", the counters that would have contradicted it were never printed, and a user running There is also What I am proposing, and it is a proposal β you two decide, not me: @RDouglasSharp has seniority here by two weeks and 27 comments, so #587 lands the decode path first. @aaristov, would you be willing to rebase this onto it and keep the observability and the If instead you two look at the two diffs and conclude the reverse β that this one is the better base and #587 should reduce to its delta β say so and I will take that. You are both closer to the Metal code than I am, and the last two overlaps on this repo were settled better by the contributors than by me. One thing I can promise: whoever ends up rebasing will not be doing it because I let it sit. Both of you have a decision within a day. Your CI had never run, incidentally β it was held in |
|
@aaristov this is wanted β it is the decode-path half of what #918 now proposes for prefill, and both should share the same expert-view plumbing β but it's marked CONFLICTING against dev. A rebase would put it back in the review queue; ping here if anything in the conflict looks like it came from our side and we'll help untangle it. |
dev implemented this PR's feature independently (fmt=4 MoE experts as `qgs`, fused attention `kvb_gs`, the `kv_b.fmt==2||fmt==4` gates), and went further: fmt=5/6/8, kv_b grid chunking, CUDA/Vulkan twins, an `mb_gs_compat` helper replacing this branch's `mgs=-1` poison, and a `!g_moe_exact` refinement on both fused-attention gates. Every conflicting hunk resolves to dev's side. Three merge defects fixed while resolving: - moe_gemv had a DUPLICATE `fmt == 4` branch (this branch's scalar version shadowing dev's vectorized one) that referenced `gsz` where dev's parameter is `qgs` -- it would not have compiled. - run_attn kept an unused `KGS`/`ng` pair; dev computes `kvng` from kvb_gs. - a coli comment referenced `backend_tag`, dropped with dev's banner rewrite. What remains of this branch over dev: - run_serve calls profile_print under PROF=1, so the cumulative METAL: / METAL-ATTN: GPU-vs-fallback counters are reachable in serve mode at all. - coli chat: stderr drained by readline() rather than read() (which only returns at EOF, so the status block always rendered empty), the ready-line regex moved to the stdout preamble where the engine actually printf's it, and [METAL]/[CUDA] whitelisted. - coli bench passes model= to banner(). Verified: make metal-test all green (incl. fmt4-g64/g128 MoE and grouped-kv_b attention), make colibri and make METAL=1 colibri both build warning-clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 'loaded in ... | resident dense:' printf moved 9209 -> 9595 when dev's commits landed in the merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflicts resolved against
|
|
Deferred to the next release (v1.7.1/v1.8.0) β not on merit, on infrastructure. Your PR is 21/22 green; the one job left is For the record, the rewrite you did in August (resolving all 37 hunks to |
What this PR does now
Three fixes that make the GPU tier's behaviour visible. None of them change
what the engine computes.
1.
run_servecan report backend counters at all (c/colibri.c, +7)profile_printonly ran on the oracle andgenerateexit paths, so thecumulative
METAL:/METAL-ATTN:/MIRROR:GPU-vs-fallback counters wereunreachable in serve mode β the mode
coli chat/coli web/coli serveall use. A served session could never show whether the GPU tier engaged or
silently fell back per block.
run_servenow takes a wall base at entry and callsprofile_printat exitunder
PROF=1. Safe against the byte protocol:stdinis at EOF by that pointand the last
END/STATframe is already out, so the stdout lines cannotinterleave with anything a client is still parsing.
2.
coli chatstatus block was always empty (c/coli)Two independent bugs, both of which silently rendered nothing:
read(), which returns only at EOF. The enginestays alive on stdin, so it never returned, the
errlog.write()after itnever ran, and the bounded wait always timed out on an empty file. Now a
readline()loop with per-line write+flush, so each status line becomesvisible as it arrives.
loaded in Xs | resident dense: Y MBis aprintf(colibri.c:9595, same ininkling.c/olmoe.c), so it arrives in the pre-READY stdout preamble β whichcmd_chatwas discarding intolambda b: None. The match could never fire.The preamble is now kept (a few hundred bytes) and the regex runs against it.
Also whitelists
[METAL]/[CUDA]in the status lines. The splash tagline(
model_banner_line) names the checkpoint, not the backend, so this engine lineis the only confirmation the GPU tier actually engaged.
3.
coli benchsplash (c/coli)banner("bench")βbanner("bench", model=a.model), sobenchnames the modelit is about to evaluate like every other command.
Test comment accuracy (
c/tests/test_backend_metal.mm, +7/-3)The fmt=8 fence test documented
moe_submit's gate asfmt != 1 && fmt != 2.On
devthe allowlist is{1,2,4,5,6}; comment corrected. No behaviour change.Merge notes
The mechanical merge of
devleft three defects, fixed here:moe_gemvended up with a duplicatefmt == 4branch β this branch'sscalar version placed above
dev's vectorized one, referencinggszwheredev's parameter is namedqgs. It would not have compiled. Removed;dev's vectorized branch stands.run_attnkept an unusedKGS/ngpair;devderiveskvngfromkvb_gs.colicomment referencedbackend_tag, which disappeared withdev'sbanner rewrite.
Not carried over: the original
backend_tag()(so the splash would notclaim "CPU" under
COLI_METAL=1).dev'smodel_banner_linerewrote that lineto name the checkpoint and omit the backend entirely, so the false claim now
survives only in
dev's no-model fallback string. Re-adding it meansredesigning
dev's function β out of scope here, worth a follow-up.Verification
make metal-testβ all green, includingdev's fmt4-g64/g128 MoE cases andthe grouped-kv_b attention cases.
make colibriandmake METAL=1 colibriβ both build warning-clean.Measured on M4 base / 34 GB. CI reports no checks on this fork branch, so the
CUDA/Vulkan paths
devadded are not exercised by the above.Original description (superseded by
dev)Problem
With an i4 snapshot (int4 weights + per-group scales = fmt=4, e.g.
GLM-5.2-i4), the Metal backend executed no decode work at all:moe_submit(backend_metal.mm) gatedfmt != 1 && fmt != 2 && fmt != 6βevery routed-expert block on every layer silently fell back to CPU.
kv_b.fmt==2exactly βattention fell back too.
Since the
[METAL] mode: β¦banner prints regardless, users saw a "GPU-enabled"build running 100% on CPU.
Fix (as originally proposed)
moe_gemvfmt=4 branch;moe_submit/coli_metal_moe_block[_begin]take ags4parameter; allowlist{1,2,4,6}.MB_BUILDcaptures the experts' group size and poisons it on mismatch.a_deqrowdecodes per-row (fmt=2) or per-group (fmt=4) kv_b scales;kvb_gsthreaded throughAttnW.run_moe_g4andrun_attn(kvb_g4=1).devlanded equivalent work with a wider format matrix (fmt=5/6/8), kv_b gridchunking, CUDA/Vulkan twins, an
mb_gs_compathelper in place of themgs=-1poison, a
!g_moe_exactrefinement on both fused-attention gates, andc/tests/test_moe_gs_guard.c. The measured numbers below were taken againstthis branch's implementation and are kept only as a record of the original
finding.
METAL:countersblocchi GPU 1248 | fallback CPU 0 | expert su GPU 9973METAL-ATTN:layer GPU 546(100% of decode layers fused)PIPE=1 DIRECT=1)π€ Generated with Claude Code