Summary
The CUDA backend works on Maxwell (Tesla M10, sm_50, compute capability 5.0). This is the first validation below the sm_80 compile-verified floor, and as far as I can tell the first Maxwell-era datapoint in the repo. The portable quant_matmul kernels are correct on sm_50 (the full cuda-test suite passes on actual hardware) and reasonably fast where they are used. All four GPUs together (note: the M10 is a PCI card which combines 4 GPUs) hold 100 % of the 10,240 experts of Qwen3.6-35B-A3B in VRAM (zero CPU misses). End-to-end decode is 2.45 tok/s, but the bottleneck is not the GPUs: it is the dense/recurrent trunk, which qwen36 keeps on a Sandy Bridge CPU with no AVX2/FMA. For reference, the same hardware does 7.3 tok/s under llama.cpp (-ngl 99 -ts 1,1,1,1) when the whole model is offloaded.
Environment
- GPU: 4x Tesla M10 (Maxwell,
sm_50, no tensor cores), 8 GB VRAM each (32 GB combined), driver 580.173.02 (host CUDA 13.0)
- CPU: 2x Intel Xeon E5-2640 @ 2.50 GHz (Sandy Bridge), 12 physical cores / 24 threads;
avx, sse4_1, sse4_2 present, avx2, fma, avx-512 absent
- RAM: 128 GB (rebalanced 64 GB / 64 GB across the two NUMA nodes)
- Storage: NVMe SSD
- OS: Ubuntu 24.04.2 LTS (Noble Numbat), kernel 6.8.0-138-generic
- Docker: 29.7.2 (client and server), with the nvidia container runtime (nvidia-container-toolkit 1.20.0). All runs were made in containers; the GPU runs used
--gpus "device=..." to expose the devices, the CPU run used none.
- Toolchain: CUDA 11.8 (the last toolkit that still targets
sm_50; Maxwell was dropped in CUDA 12.0), inside nvidia/cuda:11.8.0-devel-ubuntu22.04
- Commit:
9d5d05d (v1.12.0-4-g9d5d05d, main)
- Model:
Kreuzzelg/qwen36-35b-a3b-colibri-i4-gs64 (Qwen3.6-35B-A3B, int4 / gs=64, colibri-native container, 22 GB)
Build
make -C c qwen36 CUDA=1 CUDA_ARCH=sm_50 ARCH=native
ARCH=native is required on this CPU (not x86-64-v3): the host has no AVX2, and a -march=x86-64-v3 binary SIGILLs. With -march=native the AVX2 dense-int8 kernels compile out and the engine falls back to per-row/scalar paths. Correctness is unaffected; only CPU speed is.
The WMMA/tensor-core path is correctly skipped: it is gated compute_major >= 7 and the M10 reports sm_50, so the portable path is what runs.
Benchmark command (4-GPU warm run shown; other rows vary only COLI_GPUS, OMP_NUM_THREADS, and HEAT_FILE):
docker run --rm --gpus '"device=0,1,2,3"' -v ~/colibri:/src \
-v /mnt/ssd2/models/colibri:/models -w /src/c colibri-cuda:11.8 bash -lc \
"COLI_CUDA=1 COLI_GPUS=0,1,2,3 CUDA_EXPERT_GB=auto OMP_NUM_THREADS=12 \
OMP_WAIT_POLICY=ACTIVE OMP_PROC_BIND=close \
SNAP=/models/qwen36_i4_gs64 TOK=/models/qwen36_i4_gs64/tokenizer.json \
N_NEW=200 COLI_TIMERS=1 HEAT_FILE=/models/qwen36_i4_gs64/heat.bin \
./qwen36 256 4 prompt.txt"
Correctness
make -C c cuda-test CUDA=1 CUDA_ARCH=sm_50 on the real M10, exit 0, all pass:
backend_cuda_test q8/q4/q2/f32/e8 correctness on 1 device
ragged_attention_test rms=0 (exact)
fp8_cuda_test / mxfp4_cuda_test 0 mismatches (including the exp-255 cases)
absorb_determinism_test, weights_owned_test, cuda_fmt_trap_test, alloc_footprint_test all OK
So the portable kernels produce correct results on Maxwell, with no garbage and no drift. Real-model generation is coherent (Qwen3.6 is a reasoning model, so output is a "thinking process" followed by the answer).
CPU-vs-GPU token parity (greedy). Direct argv mode is pure greedy argmax (no temperature/top-p on this path), so I ran the same prompt through both tiers and byte-compared the decoded output:
| Run |
Decode tok/s |
TTFT |
Experts in VRAM |
VRAM hit % |
CPU-fallback misses |
CPU-only (COLI_CUDA unset) |
0.09 |
171.0 s |
n/a |
n/a |
n/a |
GPU mixed tier (COLI_GPUS=0, CUDA_EXPERT_GB=auto + heat.bin) |
2.00 |
5.5 s |
3245 / 10240 |
86.3 |
3409 |
Both produced byte-identical 64-token greedy output (258 bytes, md5 89d059e3…). The GPU run did exercise the mixed tier (3245 experts VRAM-resident, 86.3 % hit, 3409 CPU-fallback misses), yet the argmax path stayed token-exact end to end. This is a positive contrast to #510, where the HIP/RDNA2 (gfx1030) non-WMMA fallback showed a one-token greedy divergence on a mixed tier; on Maxwell/sm_50 the CUDA portable kernel and the CPU path agree on this model. As a side note, the GPU tier turns a 12.5-minute pure-CPU generation into 32 s (about 22x) on this no-AVX2 box.
Results
Prompt was the repo's canonical c/prompt.txt ("Explain the difference between a stack and a queue in three sentences."); each run decoded 200 new tokens (N_NEW=200), greedy (no temperature/top-p on the argv path). Each configuration was a single warm run (n=1), so medians and spreads do not apply.
| Config |
Decode tok/s |
VRAM hit % |
Experts resident |
CPU misses |
Peak RSS |
| 1x GPU, cold |
1.38 |
32.0 |
3245 |
>0 |
42.1 GB |
| 1x GPU, warm (heat.bin) |
1.81 |
85.7 |
3245 |
>0 |
42.0 GB |
| 4x GPU, warm, 6 thr |
2.01 |
100 |
10240/10240 |
0 |
22.8 GB |
| 4x GPU, warm, 12 thr (2nd CPU) |
2.45 |
100 |
10240/10240 |
0 |
22.3 GB |
The 4-GPU spread is the main viability result: COLI_GPUS=0,1,2,3 CUDA_EXPERT_GB=auto makes all 10,240 experts VRAM-resident (100 % hit, zero CPU misses, zero LFRU swaps), halves the MoE time (161 to 91 ms/token) and halves peak RSS (42 to 23 GB) because experts are no longer duplicated in system RAM.
Where the bottleneck actually is
Per-token (4-GPU warm, 12 threads):
| Phase |
Cost |
Runs on |
| DeltaNet trunk |
169 ms |
CPU |
| attention |
55 ms |
CPU |
| MoE |
91 ms |
all 4 GPUs |
| lm_head |
69 ms |
GPU 0 |
qwen36.c only calls qt_trunk_offer("lmhead") and qt_trunk_offer("dnproj"); the dense DeltaNet/attention path stays on the CPU. On this no-AVX2 Sandy Bridge that dense trunk is the ceiling, not the GPUs. Two controls confirm it:
- Second CPU (6 to 12 cores): 2.01 to 2.45 tok/s (+22 %), with only the CPU phases improving (attention 99 to 55 ms, DeltaNet 210 to 169 ms); the GPU phases were unchanged.
- NUMA rebalance (126/16 to 64/64 GB): no change; decode is ISA/latency-bound, not memory-bandwidth-bound.
The external comparison: llama.cpp does 7.3 tok/s on this exact hardware (Qwen3.6-35B-A3B-UD-IQ3_XXS.gguf, -ngl 99 -ts 1,1,1,1) because it offloads the dense trunk to the GPUs too. So the M10s are not the limit; the un-offloaded dense path in qwen36 is.
Summary
The CUDA backend works on Maxwell (Tesla M10,
sm_50, compute capability 5.0). This is the first validation below thesm_80compile-verified floor, and as far as I can tell the first Maxwell-era datapoint in the repo. The portablequant_matmulkernels are correct onsm_50(the fullcuda-testsuite passes on actual hardware) and reasonably fast where they are used. All four GPUs together (note: the M10 is a PCI card which combines 4 GPUs) hold 100 % of the 10,240 experts of Qwen3.6-35B-A3B in VRAM (zero CPU misses). End-to-end decode is 2.45 tok/s, but the bottleneck is not the GPUs: it is the dense/recurrent trunk, whichqwen36keeps on a Sandy Bridge CPU with no AVX2/FMA. For reference, the same hardware does 7.3 tok/s under llama.cpp (-ngl 99 -ts 1,1,1,1) when the whole model is offloaded.Environment
sm_50, no tensor cores), 8 GB VRAM each (32 GB combined), driver 580.173.02 (host CUDA 13.0)avx,sse4_1,sse4_2present,avx2,fma,avx-512absent--gpus "device=..."to expose the devices, the CPU run used none.sm_50; Maxwell was dropped in CUDA 12.0), insidenvidia/cuda:11.8.0-devel-ubuntu22.049d5d05d(v1.12.0-4-g9d5d05d,main)Kreuzzelg/qwen36-35b-a3b-colibri-i4-gs64(Qwen3.6-35B-A3B, int4 / gs=64, colibri-native container, 22 GB)Build
ARCH=nativeis required on this CPU (notx86-64-v3): the host has no AVX2, and a-march=x86-64-v3binary SIGILLs. With-march=nativethe AVX2 dense-int8 kernels compile out and the engine falls back to per-row/scalar paths. Correctness is unaffected; only CPU speed is.The WMMA/tensor-core path is correctly skipped: it is gated
compute_major >= 7and the M10 reportssm_50, so the portable path is what runs.Benchmark command (4-GPU warm run shown; other rows vary only
COLI_GPUS,OMP_NUM_THREADS, andHEAT_FILE):Correctness
make -C c cuda-test CUDA=1 CUDA_ARCH=sm_50on the real M10, exit 0, all pass:backend_cuda_testq8/q4/q2/f32/e8 correctness on 1 deviceragged_attention_testrms=0 (exact)fp8_cuda_test/mxfp4_cuda_test0 mismatches (including the exp-255 cases)absorb_determinism_test,weights_owned_test,cuda_fmt_trap_test,alloc_footprint_testall OKSo the portable kernels produce correct results on Maxwell, with no garbage and no drift. Real-model generation is coherent (Qwen3.6 is a reasoning model, so output is a "thinking process" followed by the answer).
CPU-vs-GPU token parity (greedy). Direct argv mode is pure greedy argmax (no temperature/top-p on this path), so I ran the same prompt through both tiers and byte-compared the decoded output:
COLI_CUDAunset)COLI_GPUS=0,CUDA_EXPERT_GB=auto+heat.bin)Both produced byte-identical 64-token greedy output (258 bytes, md5
89d059e3…). The GPU run did exercise the mixed tier (3245 experts VRAM-resident, 86.3 % hit, 3409 CPU-fallback misses), yet the argmax path stayed token-exact end to end. This is a positive contrast to #510, where the HIP/RDNA2 (gfx1030) non-WMMA fallback showed a one-token greedy divergence on a mixed tier; on Maxwell/sm_50the CUDA portable kernel and the CPU path agree on this model. As a side note, the GPU tier turns a 12.5-minute pure-CPU generation into 32 s (about 22x) on this no-AVX2 box.Results
Prompt was the repo's canonical
c/prompt.txt("Explain the difference between a stack and a queue in three sentences."); each run decoded 200 new tokens (N_NEW=200), greedy (no temperature/top-p on the argv path). Each configuration was a single warm run (n=1), so medians and spreads do not apply.The 4-GPU spread is the main viability result:
COLI_GPUS=0,1,2,3 CUDA_EXPERT_GB=automakes all 10,240 experts VRAM-resident (100 % hit, zero CPU misses, zero LFRU swaps), halves the MoE time (161 to 91 ms/token) and halves peak RSS (42 to 23 GB) because experts are no longer duplicated in system RAM.Where the bottleneck actually is
Per-token (4-GPU warm, 12 threads):
qwen36.conly callsqt_trunk_offer("lmhead")andqt_trunk_offer("dnproj"); the dense DeltaNet/attention path stays on the CPU. On this no-AVX2 Sandy Bridge that dense trunk is the ceiling, not the GPUs. Two controls confirm it:The external comparison: llama.cpp does 7.3 tok/s on this exact hardware (
Qwen3.6-35B-A3B-UD-IQ3_XXS.gguf,-ngl 99 -ts 1,1,1,1) because it offloads the dense trunk to the GPUs too. So the M10s are not the limit; the un-offloaded dense path inqwen36is.