Skip to content

[datapoint] Qwen3.6-35B-A3B on Tesla M10 #1652

Description

@ggeorgovassilis

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    benchmarkDatapoint di misurazione hardware

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions