Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Metal: fix grid-size truncation in coli_metal_gemm at large prefill (long-context corruption)#488

Merged
JustVugg merged 3 commits into
JustVugg:devJustVugg/colibri:devfrom
RDouglasSharp:fix/metal-gemm-grid-truncationRDouglasSharp/colibri:fix/metal-gemm-grid-truncationCopy head branch name to clipboard
Jul 22, 2026
Merged

Metal: fix grid-size truncation in coli_metal_gemm at large prefill (long-context corruption)#488
JustVugg merged 3 commits into
JustVugg:devJustVugg/colibri:devfrom
RDouglasSharp:fix/metal-gemm-grid-truncationRDouglasSharp/colibri:fix/metal-gemm-grid-truncationCopy head branch name to clipboard

Conversation

@RDouglasSharp

Copy link
Copy Markdown
Contributor

Metal: fix grid-size truncation in coli_metal_gemm at large prefill (long-context corruption)

Summary

On the Metal backend, dense GEMMs with a very large output (S × O elements) are dispatched as a
single grid. Past a device threadgroup-grid limit the grid is silently truncated — the tail
output rows are never computed and keep whatever was in the buffer. In the engine that buffer is
reused scratch, so the stale contents are nondeterministic garbage → corrupted generation at long
context
. This fixes it by chunking the dispatch so no single launch exceeds a safe grid size. No
kernel change; output is bit-exact vs. CPU.

Motivation — this is what makes Metal + OpenCode/agent backends usable

colibri on Metal (COLI_METAL=1) is currently unusable as an OpenCode / coding-agent backend
because of this bug. Agent front-ends prepend a large fixed system header — tool declarations, rules,
worked examples — that is several thousand tokens before the user types anything and cannot be
trimmed. That header alone requires a large context (we run CTX=40960 just to fit it) and drives a
big prefill on the first message of every session. That prefill is exactly the regime that
crosses the grid limit, so the GPU path returns garbage from the first token, while the CPU path is
correct but far slower. In other words: without this fix, Metal + OpenCode simply does not work; with
it, the GPU path produces coherent output and well-formed tool calls at real agentic context sizes.
(Nobody upstream hit this because Metal validation uses tiny 8–13-token prompts — see "Why it evaded
detection".)

Symptom / impact

  • Long prompts on COLI_METAL=1 produce corrupted output (dropped/blank tokens, degrading to token
    salad); the identical prompt is correct on CPU (COLI_METAL=0) and correct on Metal at short
    lengths.
  • Because the trigger is the fixed agent header, it hits the first turn of every OpenCode session
    on Metal — i.e. the GPU backend is effectively dead for coding-agent workloads until this lands.

Root cause

matmul_qt_ex routes dense matmuls with S ≥ COLI_METAL_GEMM_MIN to coli_metal_gemm, which
dispatches NT = S*O output elements as one grid of (NT+3)/4 threadgroups. The largest projection
(kv_b, O≈28672) crosses the device grid limit at large S: on M-series, kv_b at S=4376
(grid ≈ 3.1e7 tg) computes fully, but at S=7478 (grid ≈ 5.4e7 tg) the tail is not computed. It is
not an out-of-bounds (Metal shader validation is silent) and not dispatch-arithmetic
overflow (NT stays ≈ 9e7 ≪ 2³¹) — it's the raw grid size exceeding what a single dispatch schedules.

Fix

Chunk coli_metal_gemm over output rows so each dispatch is ≤ 2²⁵ elements (grid ≤ 2²³ tg, ~4× under
the observed-clean bound), using g_gx/g_gy buffer offsets — no shader change, chunks write
disjoint output ranges, negligible overhead, and it's a no-op for small S (single chunk) and decode
(S=1). Gated by COLI_GEMM_CHUNK (default on; =0 restores the old single-dispatch path for A/B).

Reproduction & test (seconds, no model)

New make gemm-test (tests/test_gemm_largebatch.mm) calls coli_metal_gemm directly across a size
sweep of real GLM shapes vs. an OMP CPU reference, and includes a GPU-vs-GPU determinism check:

  • Before the fix (COLI_GEMM_CHUNK=0): kv_b S=7478nerr = 1.00e+00 (*** CORRUPT).
  • After the fix (default): all shapes/sizes clean, nerr ~1e-6.

Validation

  • make gemm-test: all clean after the fix; the S=7478 kv_b case is corrupt with COLI_GEMM_CHUNK=0
    and clean with it on (same binary A/B).
  • End-to-end: a full ~7478-token OpenCode-scale prefill produces coherent output + well-formed tool
    calls (previously token salad).

Why it evaded detection

The truncation is at the grid boundary, so it's nondeterministic across process launches (a
given launch may land just under or just over) and only manifests at agentic prefill sizes.
Existing Metal kernel tests exercise S ≤ 64; nothing upstream drives a multi-thousand-token prefill
through Metal, so it never surfaced.

Scope / not included

  • The batched MoE-expert GEMV (moe_submit/moe_gemv) has the same single-dispatch pattern and
    could hit the limit at very large per-block row counts; not triggered in the confirmed repro
    (MoE-on-GPU alone is clean at these sizes), so it's left for a follow-up with its own test.
  • Multi-turn KV reuse / prefill latency (re-prefilling the header each turn) is a separate
    performance limitation, not addressed here.
Investigation notes — hypotheses ruled out (full log attached)

Ruled out en route (each by a measured run; full forensic detail in the attached investigation log):
DSA sparse attention (model has no indexer weights), RoPE extrapolation (max_position 1M, per-position
angles), CTX-scaled allocation, int32 NT overflow, CPU-attention score buffers (#110/#117, target
8192), the residency set (COLI_METAL_RESSET on/off both corrupt), the async I/O pipeline (PIPE=0
still corrupt), pilot prefetch (off by default), a GEMM+MoE interaction (corrupts with MoE on CPU),
and macOS-compressor eviction of scratch (setBuffer:-bound buffers are auto-resident). The 2×2
GEMM×MoE placement matrix isolated corruption to dense GEMM on GPU; the standalone gemm-test
then proved the GEMM is bit-exact in isolation, localizing the fault to the single-dispatch grid
size — deterministically reproduced once the sweep reached kv_b S=7478.

Separately noticed (not fixed here): openai_server.py's --engine default still points at
glm after the glm.ccolibri.c rename (#391); direct invocation is broken on a clean checkout,
and silently runs a stale glm if one exists. Worth a one-line fix (default to colibri, fall back
to glm).

@RDouglasSharp

Copy link
Copy Markdown
Contributor Author

Details of the investigation that led to this fix, and some remaining issues using OpenCode with Colibri server:

INVESTIGATION_LOG_metal_moe_race.md

@JustVugg

Copy link
Copy Markdown
Owner

To merge this I need validation data I can't produce myself (no such GPU here). The bar every backend holds: (1) correctness first — a teacher-forcing token-exact check on a tiny model (SNAP=<tiny> TF=1 ./coli ... → N/32 vs the CPU oracle), or at minimum a clearly-coherent chat transcript; (2) perf — before/after tok/s on the same prompt + your GPU/driver/runtime version. Rebase on current dev (CI is green), get CI passing, post that data, and I merge. Correctness matters more than speed — #298 was a GPU path that looked right and produced garbage.

Specifically for this grid-size/long-context fix: a before/after on a long prefill (the case that corrupted) showing coherent output after — ideally token-exact vs CPU on a tiny model. Thanks @RDouglasSharp.

…fill

Large dense GEMMs (NT=S*O elements) were dispatched as a single grid; past the
device threadgroup-grid limit the tail output rows were silently never computed,
leaving stale scratch -> nondeterministic long-context corruption under
COLI_METAL=1 (identical prompt correct on CPU / at short lengths). Chunk the
dispatch to <=2^25 elements via g_gx/g_gy buffer offsets: no kernel change,
disjoint output ranges, negligible overhead, no-op for small S and decode.
Gated COLI_GEMM_CHUNK (default on; =0 restores old path for A/B).

Adds tests/test_gemm_largebatch.mm (make gemm-test): kv_b S=7478 nerr=1.0
before, clean after.
@JustVugg
JustVugg force-pushed the fix/metal-gemm-grid-truncation branch from 16e339c to a0b929c Compare July 21, 2026 21:37
@JustVugg

Copy link
Copy Markdown
Owner

Hi @RDouglasSharpI rebased this onto dev for you and force-pushed to your branch (you had maintainerCanModify on, thanks). Contributors shouldn't pay for our merge order, so we do the rebases here.

The only conflict was c/Makefile: dev rewrote the cuda-test recipe in the AMD/HIP single-source refactor (#339) to use $(GPUCC)/$(GPUFLAGS)/backend_gpu_compat.h. Resolution keeps both — your new gemm-test target verbatim, and dev's newer cuda-test. Nothing of yours was dropped; the diff is still exactly your 3 files (Makefile +6, backend_metal.mm, tests/test_gemm_largebatch.mm). PR is MERGEABLE now.

I read the fix and I like it: chunking to 2^25 elements per dispatch with COLI_GEMM_CHUNK=0 as an escape hatch means the bug can be A/B'd on a single binary — that's exactly the kind of gate we want (we got burned recently merging a cache PR that was byte-identical but silently killed a fast path, #490).

One thing before we merge: I can't validate this. I'm on Linux — the Metal path never compiles here, so all I can honestly say is "the CPU build is unchanged and the Makefile parses". Could you run your own test on your Mac and paste the output?

make gemm-test                     # with the fix
COLI_GEMM_CHUNK=0 ./gemm_largebatch_test   # pre-fix behavior, same binary

Fail-before / pass-after in one paste and I'll merge. If you have a real long-context prefill handy (the S=7478 case from your report), that on top would be ideal.

@RDouglasSharp

RDouglasSharp commented Jul 21, 2026 via email

Copy link
Copy Markdown
Contributor Author

@RDouglasSharp

Copy link
Copy Markdown
Contributor Author

(1) Correctness — teacher-forcing, token-exact, GPU vs CPU. Tiny oracle built with tools/make_glm_oracle.py. At f32 the engine matches the transformers oracle exactly (SNAP=./glm_tiny TF=1 ./colibri 64 16 16 → 32/32). To put the work on the patched coli_metal_gemm I ran int8 (S=32 ≥ g_metal_gemm_min=16):

COLI_METAL=1 … 64 8 8 → 30/32 vs oracle; METAL: blocchi GPU 4 | fallback CPU 0 | expert su GPU 12
COLI_METAL=0 … 64 8 8 → 30/32 vs oracle

Metal and CPU are token-identical at all 32 positions — the same two positions diverge from the bf16 oracle (pos 5→197, pos 25→136) on both paths, i.e. that 2/32 is int8 quantization vs bf16, not a GPU artifact. The GPU path reproduces CPU bit-for-bit with the fix in place.

@RDouglasSharp

Copy link
Copy Markdown
Contributor Author

git fetch origin
git reset --hard origin/fix/metal-gemm-grid-truncation
./gemm_largebatch_test
clang++ -x objective-c++ -std=gnu++17 -fobjc-arc -O3 -Xclang -fopenmp -I/opt/homebrew/opt/libomp/include tests/test_gemm_largebatch.mm backend_metal.mm -L/opt/homebrew/opt/libomp/lib -lomp -framework Metal -framework Foundation -o gemm_largebatch_test
./gemm_largebatch_test
coli_metal_gemm large-batch sweep (GPU vs CPU, + determinism):
gate/up (O2048,I6144) S=512 O=2048 I=6144 nerr=3.27e-06 det:same ok first-bad row=19 col=1182 (gpu=0.0001163 cpu=0.0001136)
gate/up (O2048,I6144) S=2153 O=2048 I=6144 nerr=3.42e-06 det:same ok first-bad row=4 col=513 (gpu=0.0001721 cpu=0.0001749)
gate/up (O2048,I6144) S=4376 O=2048 I=6144 nerr=3.58e-06 det:same ok first-bad row=7 col=2004 (gpu=0.0001503 cpu=0.0001487)
COLI_GEMM_CHUNK=0 ./gemm_largebatch_test
gate/up (O2048,I6144) S=7478 O=2048 I=6144 nerr=3.53e-06 det:same ok first-bad row=18 col=920 (gpu=-6.424e-05 cpu=-6.542e-05)

down (O6144,I2048) S=512 O=6144 I=2048 nerr=1.84e-06 det:same ok first-bad row=2 col=1435 (gpu=4.094e-05 cpu=4.183e-05)
down (O6144,I2048) S=2153 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=3 col=3630 (gpu=-7.874e-08 cpu=8.907e-07)
down (O6144,I2048) S=4376 O=6144 I=2048 nerr=2.05e-06 det:same ok first-bad row=1 col=2259 (gpu=4.125e-05 cpu=4.076e-05)
down (O6144,I2048) S=7478 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=6 col=77 (gpu=4.006e-05 cpu=3.958e-05)

kv_b (O28672,I512) S=512 O=28672 I=512 nerr=1.25e-06 det:same ok first-bad row=3 col=28289 (gpu=1.98e-08 cpu=8.291e-08)
kv_b (O28672,I512) S=2153 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=13903 (gpu=-6.214e-08 cpu=-3.528e-08)
kv_b (O28672,I512) S=4376 O=28672 I=512 nerr=1.07e-06 det:same ok first-bad row=4 col=6386 (gpu=-2.123e-05 cpu=-2.159e-05)
kv_b (O28672,I512) S=7478 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=24711 (gpu=8.057e-08 cpu=2.392e-07)

GEMM large-batch: all clean

COLI_GEMM_CHUNK=0 ./gemm_largebatch_test
coli_metal_gemm large-batch sweep (GPU vs CPU, + determinism):
gate/up (O2048,I6144) S=512 O=2048 I=6144 nerr=3.27e-06 det:same ok first-bad row=19 col=1182 (gpu=0.0001163 cpu=0.0001136)
gate/up (O2048,I6144) S=2153 O=2048 I=6144 nerr=3.42e-06 det:same ok first-bad row=4 col=513 (gpu=0.0001721 cpu=0.0001749)
gate/up (O2048,I6144) S=4376 O=2048 I=6144 nerr=3.58e-06 det:same ok first-bad row=7 col=2004 (gpu=0.0001503 cpu=0.0001487)
gate/up (O2048,I6144) S=7478 O=2048 I=6144 nerr=3.53e-06 det:same ok first-bad row=18 col=920 (gpu=-6.424e-05 cpu=-6.542e-05)

down (O6144,I2048) S=512 O=6144 I=2048 nerr=1.84e-06 det:same ok first-bad row=2 col=1435 (gpu=4.094e-05 cpu=4.183e-05)
down (O6144,I2048) S=2153 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=3 col=3630 (gpu=-7.874e-08 cpu=8.907e-07)
down (O6144,I2048) S=4376 O=6144 I=2048 nerr=2.05e-06 det:same ok first-bad row=1 col=2259 (gpu=4.125e-05 cpu=4.076e-05)
down (O6144,I2048) S=7478 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=6 col=77 (gpu=4.006e-05 cpu=3.958e-05)

kv_b (O28672,I512) S=512 O=28672 I=512 nerr=1.25e-06 det:same ok first-bad row=3 col=28289 (gpu=1.98e-08 cpu=8.291e-08)
kv_b (O28672,I512) S=2153 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=13903 (gpu=-6.214e-08 cpu=-3.528e-08)
kv_b (O28672,I512) S=4376 O=28672 I=512 nerr=1.07e-06 det:same ok first-bad row=4 col=6386 (gpu=-2.123e-05 cpu=-2.159e-05)
kv_b (O28672,I512) S=7478 O=28672 I=512 nerr=1.00e+00 det:same *** CORRUPT first-bad row=1 col=24711 (gpu=8.057e-08 cpu=2.392e-07)

GEMM large-batch: CORRUPTION DETECTED

@RDouglasSharp

Copy link
Copy Markdown
Contributor Author

I am happy to post back to back to back CPU vs METAL CHUNK-0 vs METAL with fix, but I am in the middle of upgrading to the improved 4-bit model (downloading in progress), so it's going to take a while. The summary of the results for a very long prompt on the older 4-bit model: without the fix, catastrophic corruption, after the fix, absolute prose quality is limited by the pre-#225 checkpoint, and does not perfectly match CPU result, but resembles CPU result.

@JustVugg

Copy link
Copy Markdown
Owner

Reviewed the change itself, since the one thing I can contribute here is a careful read — I have no Apple silicon, so the hardware evidence has to come from you, and it does: a same-binary A/B (COLI_GEMM_CHUNK=0nerr=1.00e+00 corrupt, default → nerr~1e-6 clean) plus a standalone test anyone with a Mac can rerun in seconds. That is exactly the standard this project asks of GPU changes, and you met it before being asked. The investigation — bisecting to the grid boundary, ruling out both OOB and dispatch-arithmetic overflow, and explaining why it evaded detection (existing kernel tests stop at S≤64) — is the kind of writeup that makes a fix reviewable by someone who can't run it.

The chunking logic reads correct to me: chunks write disjoint g_gy row ranges, the kernel gets ch where it used to get S with the base pointers shifted to match, NT=ch*O ≤ 2²⁵ can't overflow int, and it's a genuine no-op at S=1, so the decode path is untouched. That last point is what makes this low-risk to merge.

One real problem, in the disable path:

const int64_t NT_MAX = chunk_on ? ((int64_t)1<<25) : ((int64_t)1<<62);
int CH=(int)(NT_MAX/O);

With chunking off, NT_MAX/O is about 1.6e14 for kv_b (O=28672) — far past INT_MAX, so the cast to int is undefined. On your machine it evidently landed large and positive, which is why CH>S clamped to S and you reproduced the corruption. But if it truncates negative on another toolchain, CH<1 forces CH=1, you get one row per dispatch, and the "before" case comes out clean — the A/B silently stops demonstrating the bug it exists to demonstrate. Ironic failure mode for an escape hatch.

Clamping before the narrowing fixes it:

int64_t ch64 = NT_MAX/O; if(ch64<1) ch64=1; if(ch64>S) ch64=S; int CH=(int)ch64;

One question: the comment justifies the buffer offsets as 16B-aligned because I and O are multiples of 4. That holds for GLM's shapes, but is it guaranteed for every model this path serves (a tiny oracle checkpoint, a future container with odd dims)? If it isn't structurally true, an assert or a fallback beats a comment.

Fix the cast and I'm ready to merge this — the corruption it removes makes Metal usable for agent workloads, and CI is green. Thanks also for flagging the --engine default while you were in there; that was a real bug on clean checkouts and it's fixed in dev now (#526).

@RDouglasSharp

Copy link
Copy Markdown
Contributor Author

Nice catch! Fixed in 70f06cd — clamped in 64-bit before narrowing, exactly as you suggested:

int64_t ch64=NT_MAX/O; if(ch64<1) ch64=1; if(ch64>S) ch64=S; int CH=(int)ch64;

After the clamp CH ≤ S ≤ INT_MAX, so the conversion is defined on any toolchain. You were right that this was the dangerous one — a negative truncation would have given CH=1, one row per dispatch, and the "before" case would have come out clean, silently defanging the A/B it exists for.

Re-ran the A/B on the fixed binary to confirm the escape hatch still demonstrates the bug:

make gemm-test
clang++ -x objective-c++ -std=gnu++17 -fobjc-arc -O3 -Xclang -fopenmp -I/opt/homebrew/opt/libomp/include tests/test_gemm_largebatch.mm backend_metal.mm -L/opt/homebrew/opt/libomp/lib -lomp -framework Metal -framework Foundation -o gemm_largebatch_test
./gemm_largebatch_test
coli_metal_gemm large-batch sweep (GPU vs CPU, + determinism):
gate/up (O2048,I6144) S=512 O=2048 I=6144 nerr=3.27e-06 det:same ok first-bad row=19 col=1182 (gpu=0.0001163 cpu=0.0001136)
gate/up (O2048,I6144) S=2153 O=2048 I=6144 nerr=3.42e-06 det:same ok first-bad row=4 col=513 (gpu=0.0001721 cpu=0.0001749)
gate/up (O2048,I6144) S=4376 O=2048 I=6144 nerr=3.58e-06 det:same ok first-bad row=7 col=2004 (gpu=0.0001503 cpu=0.0001487)
gate/up (O2048,I6144) S=7478 O=2048 I=6144 nerr=3.53e-06 det:same ok first-bad row=18 col=920 (gpu=-6.424e-05 cpu=-6.542e-05)

down (O6144,I2048) S=512 O=6144 I=2048 nerr=1.84e-06 det:same ok first-bad row=2 col=1435 (gpu=4.094e-05 cpu=4.183e-05)
down (O6144,I2048) S=2153 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=3 col=3630 (gpu=-7.874e-08 cpu=8.907e-07)
down (O6144,I2048) S=4376 O=6144 I=2048 nerr=2.05e-06 det:same ok first-bad row=1 col=2259 (gpu=4.125e-05 cpu=4.076e-05)
down (O6144,I2048) S=7478 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=6 col=77 (gpu=4.006e-05 cpu=3.958e-05)

kv_b (O28672,I512) S=512 O=28672 I=512 nerr=1.25e-06 det:same ok first-bad row=3 col=28289 (gpu=1.98e-08 cpu=8.291e-08)
kv_b (O28672,I512) S=2153 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=13903 (gpu=-6.214e-08 cpu=-3.528e-08)
kv_b (O28672,I512) S=4376 O=28672 I=512 nerr=1.07e-06 det:same ok first-bad row=4 col=6386 (gpu=-2.123e-05 cpu=-2.159e-05)
kv_b (O28672,I512) S=7478 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=24711 (gpu=8.057e-08 cpu=2.392e-07)

GEMM large-batch: all clean
(.venv) doug@Mac c % ./gemm_largebatch_test
coli_metal_gemm large-batch sweep (GPU vs CPU, + determinism):
gate/up (O2048,I6144) S=512 O=2048 I=6144 nerr=3.27e-06 det:same ok first-bad row=19 col=1182 (gpu=0.0001163 cpu=0.0001136)
gate/up (O2048,I6144) S=2153 O=2048 I=6144 nerr=3.42e-06 det:same ok first-bad row=4 col=513 (gpu=0.0001721 cpu=0.0001749)
gate/up (O2048,I6144) S=4376 O=2048 I=6144 nerr=3.58e-06 det:same ok first-bad row=7 col=2004 (gpu=0.0001503 cpu=0.0001487)
gate/up (O2048,I6144) S=7478 O=2048 I=6144 nerr=3.53e-06 det:same ok first-bad row=18 col=920 (gpu=-6.424e-05 cpu=-6.542e-05)

down (O6144,I2048) S=512 O=6144 I=2048 nerr=1.84e-06 det:same ok first-bad row=2 col=1435 (gpu=4.094e-05 cpu=4.183e-05)
down (O6144,I2048) S=2153 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=3 col=3630 (gpu=-7.874e-08 cpu=8.907e-07)
down (O6144,I2048) S=4376 O=6144 I=2048 nerr=2.05e-06 det:same ok first-bad row=1 col=2259 (gpu=4.125e-05 cpu=4.076e-05)
down (O6144,I2048) S=7478 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=6 col=77 (gpu=4.006e-05 cpu=3.958e-05)

kv_b (O28672,I512) S=512 O=28672 I=512 nerr=1.25e-06 det:same ok first-bad row=3 col=28289 (gpu=1.98e-08 cpu=8.291e-08)
kv_b (O28672,I512) S=2153 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=13903 (gpu=-6.214e-08 cpu=-3.528e-08)
kv_b (O28672,I512) S=4376 O=28672 I=512 nerr=1.07e-06 det:same ok first-bad row=4 col=6386 (gpu=-2.123e-05 cpu=-2.159e-05)
kv_b (O28672,I512) S=7478 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=24711 (gpu=8.057e-08 cpu=2.392e-07)

GEMM large-batch: all clean
(.venv) doug@Mac c % COLI_GEMM_CHUNK=0 ./gemm_largebatch_test
coli_metal_gemm large-batch sweep (GPU vs CPU, + determinism):
gate/up (O2048,I6144) S=512 O=2048 I=6144 nerr=3.27e-06 det:same ok first-bad row=19 col=1182 (gpu=0.0001163 cpu=0.0001136)
gate/up (O2048,I6144) S=2153 O=2048 I=6144 nerr=3.42e-06 det:same ok first-bad row=4 col=513 (gpu=0.0001721 cpu=0.0001749)
gate/up (O2048,I6144) S=4376 O=2048 I=6144 nerr=3.58e-06 det:same ok first-bad row=7 col=2004 (gpu=0.0001503 cpu=0.0001487)
gate/up (O2048,I6144) S=7478 O=2048 I=6144 nerr=3.53e-06 det:same ok first-bad row=18 col=920 (gpu=-6.424e-05 cpu=-6.542e-05)

down (O6144,I2048) S=512 O=6144 I=2048 nerr=1.84e-06 det:same ok first-bad row=2 col=1435 (gpu=4.094e-05 cpu=4.183e-05)
down (O6144,I2048) S=2153 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=3 col=3630 (gpu=-7.874e-08 cpu=8.907e-07)
down (O6144,I2048) S=4376 O=6144 I=2048 nerr=2.05e-06 det:same ok first-bad row=1 col=2259 (gpu=4.125e-05 cpu=4.076e-05)
down (O6144,I2048) S=7478 O=6144 I=2048 nerr=2.04e-06 det:same ok first-bad row=6 col=77 (gpu=4.006e-05 cpu=3.958e-05)

kv_b (O28672,I512) S=512 O=28672 I=512 nerr=1.25e-06 det:same ok first-bad row=3 col=28289 (gpu=1.98e-08 cpu=8.291e-08)
kv_b (O28672,I512) S=2153 O=28672 I=512 nerr=1.05e-06 det:same ok first-bad row=1 col=13903 (gpu=-6.214e-08 cpu=-3.528e-08)
kv_b (O28672,I512) S=4376 O=28672 I=512 nerr=1.07e-06 det:same ok first-bad row=4 col=6386 (gpu=-2.123e-05 cpu=-2.159e-05)
kv_b (O28672,I512) S=7478 O=28672 I=512 nerr=1.00e+00 det:same *** CORRUPT first-bad row=1 col=24711 (gpu=8.057e-08 cpu=2.392e-07)

GEMM large-batch: CORRUPTION DETECTED

@RDouglasSharp

Copy link
Copy Markdown
Contributor Author

Also corrected two stale comments in the test that the sweep above contradicts: it labelled S=4376 the "corrupt bracket" and described an "S>=~2400 fault." Both wrong — the trigger is the dispatch grid (NT=S·O), not S alone, which is why only kv_b (O=28672) crosses the limit and only at S=7478, while gate/up/down stay clean at every S in the sweep. Comment-only change, no behaviour.

pull Bot pushed a commit to danielabelski/colibri that referenced this pull request Jul 22, 2026
python3 openai_server.py --model <dir> looked for a binary named 'glm' next
to itself. Since JustVugg#391 the build produces 'colibri', so direct invocation was
broken on any clean checkout -- it only appeared to work in trees that still
had a stale glm from an older build. Spotted by @RDouglasSharp while working
on JustVugg#488.

Resolve the engine by probing colibri / colibri.exe first and falling back to
glm / glm.exe, the same order the coli launcher uses, so old trees keep
starting. Verified by removing the stale glm and running the gateway: it
resolves to colibri and the engine launches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JustVugg
JustVugg merged commit f1b0f7e into JustVugg:dev Jul 22, 2026
9 checks passed
@JustVugg

Copy link
Copy Markdown
Owner

Merged. The clamp is exactly right, and thank you for going further than asked: correcting the two stale comments in the test — the ones that blamed S when your own sweep shows the trigger is the dispatch grid NT=S·O — is the kind of care that makes a fix trustworthy to someone who can't run it. A test whose comments contradict its own output is a trap for the next reader.

For the record, what carried this over the line without Apple silicon on my side: a same-binary A/B (COLI_GEMM_CHUNK=0 corrupt / default clean), a standalone reproducer that runs in seconds without the model, an escape hatch if it ever misbehaves, and a genuine no-op at S=1 so the decode path everyone uses is untouched. That's the standard I'd like GPU changes to meet, and you set it before being asked.

Also thanks for spotting the --engine default while you were in there — that was a real breakage on clean checkouts, fixed in #526 and shipped in v1.1.1.

monotophic added a commit to monotophic/colibri that referenced this pull request Jul 23, 2026
…the falloc->qalloc trade-off

- test_gemm_largebatch.mm predates the 9-arg coli_metal_gemm prototype; gs=0
  selects per-row scales, the exact pre-fmt=4 semantics its fixtures assume.
- The fmt=4 scale-buffer qalloc site is outside #ifdef COLI_METAL: on CPU-only
  builds it trades falloc's checked-exit for qalloc's unchecked malloc,
  consistent with qsalloc for fmt 1/2/3 -- now said in the comment, with a
  note that fmt=5's group scales still use falloc (Metal-inert, pre-existing).
monotophic added a commit to monotophic/colibri that referenced this pull request Jul 23, 2026
…the falloc->qalloc trade-off

- test_gemm_largebatch.mm predates the 9-arg coli_metal_gemm prototype; gs=0
  selects per-row scales, the exact pre-fmt=4 semantics its fixtures assume.
- The fmt=4 scale-buffer qalloc site is outside #ifdef COLI_METAL: on CPU-only
  builds it trades falloc's checked-exit for qalloc's unchecked malloc,
  consistent with qsalloc for fmt 1/2/3 -- now said in the comment, with a
  note that fmt=5's group scales still use falloc (Metal-inert, pre-existing).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.