Repository navigation
Conversation
…Intel Arc (experimental)
…n this tree, and the default build mode
|
Thank you, @BHCC2025. This comes after #523 and #547, and two parts can't come in:
MLX 4-bit and EXL3 on #496's decoder can follow once #523 and #547 have landed, within the lean rules: files under 600 lines and one-line comments. |
|
Hello @BHCC2025, Thank you for Qwen3.8-27B on the Arc, and for the row checks on MLX, EXL3, and Q5_K. The KV cache defaults to q8. bf16 runs only when TENSORFOLD_XPU_KV=bf16. Please default to bf16, and make q8 and q4 opt-in. A reconstructed value that agrees within rounding is a different cache. EXL3 projections use the decode kernels below 256 rows and the prefill GEMM at 256 (xpu_win.zig:346, xpu_win.zig:355). The compare already moved one top-1 in eight steps at that seam. Please use one EXL3 prompt kernel at every length. The server facts are still exact_width 1, mtp false, and max_streams 1 (xpu_lane.zig:77). This stays blocked on the #547 host: one stream, one row, no draft head, no prompt cache. zig/kernels/xpu/ggml_tables.h is 632 lines. Please split that generated table under 600. A push with the bf16 default and the single EXL3 kernel is the update we need. Thank you again. |
The CLI (--kv), the server (TENSORFOLD_XPU_KV) and the engine default to a bf16 cache. The MTP head's cache follows the target's format by default (it was q4 past 16K with a bf16 target); MTP_KV still overrides it.
…and its generator in the tree tools/gen_ggml_tables.py writes ggml_tables.h and ggml_tables_iq1s.h (included from it); the values are byte-identical to the 632-line table, and the kernel build lists both headers as inputs.
…o not depend on how it is cut Prompt rows go through Model.promptRows: when the window has prompt kernels (--prefill above 16), every prompt piece, including a last piece of 16 rows or fewer, runs as a prompt chunk. EXL3 and fp16 projections take their prefill GEMMs at every chunk size (the switch at 256 rows is gone), and MLX and GGUF take theirs for prompt pieces of any size. Decode and verify windows keep the decode kernels. qwen_seam reads .gguf files and no longer takes differ=.
|
@ashhart, thank you. All three are in, on top of this branch's head: KV cache defaults to bf16 ( One prompt kernel at every length (
Unchanged: windows of 2-16 rows equal one-row decode (
The server facts stay as they are until the #547 lane host is updated; that's next. |
|
@ashhart, closing this in favour of #802, which already covers Qwen3.8-27B on Arc with a better runner. Thanks for the reviews here. Most of #550 is now in #802 or no longer needed. The one piece #802 lacks is EXL3 weights. On the B70 our EXL3 decode runs at 30.8 tok/s against #802's 4-bit at 34.9, and its only gain is about 4 GiB less VRAM. That isn't worth a separate path, so further Qwen-on-Arc work from us will go into #802's runner. The Nemotron side continues in #523 and #547, both now rebased onto #802. |
|
Hello @BHCC2025, Thank you for the three fixes. The KV cache defaults to bf16, and q8 and q4 are opt-in (xpu_qwen.zig:17). Every prompt piece takes the prompt kernels (xpu_model.zig:139, xpu_win.zig:344). ggml_tables.h is 375 lines, and the rest is ggml_tables_iq1s.h. Intel Arc is accepted through #802's server. One Level Zero runtime, and one 27B forward. EXL3 (xpu_qwen.zig:9), GGUF on the CLI (xpu_main.zig:209), and the MTP head (xpu_mtp.zig:158) are ahead of that server. It loads MLX affine 4/64 and GPTQ 4/128, and it drafts with DFlash2. Those three come onto #802's runtime. The copy index and the auto drafter (xpu_spec.zig:51, xpu_spec.zig:183) come with the MTP head. This server stays one stream, one row, and no draft head (xpu_lane.zig:77). Thank you again. |
Part 3 of 3, stacked on part 1 (#523) and part 2 (#547). Issue: #522. Parts 1 and 2 are not merged yet, so this branch contains their commits; review the last commit only (91 files).
What this adds
Qwen3.8-27B (Gated DeltaNet + attention, 64 layers) on the Intel GPU engine from three weight formats behind one linear layer: MLX affine 4-bit,
EXL3 (trellis) and GGUF (ggml block quantization: Q2_K, Q4_K, Q5_K, Q6_K, Q8_0, IQ1_M, IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S, IQ4_XS, IQ4_NL).
(q8 is the default); prompt windows on the matrix engine (
--prefill ROWS|auto); the delta-rule window kernel with 2 or 4 value rows a sub-group.The copy drafter finds its match through an n-gram index, so the cost of a draft no longer grows with the history; it returns the same match as the full scan up to a 512-candidate chain bound (
qwen_copyidx: 0 of 9,000 calls differ from the scan, 0.0001 ms a call against 0.72 ms for the scan at 123K tokens).tensorfold-native serve MODEL --backend xpunow also opens Qwen3.8-27B MLX 4-bit and EXL3 directories (one stream at a time, host sampling, no drafts), through the samelane backend as Nemotron. The server resolves directories, so GGUF files are served by the CLI only. The KV format is the environment variable
TENSORFOLD_XPU_KV(bf16,q8,q4; defaultq8).--contextis refused with a message that says so.No claim is made for the 262,144 window.
zig/src/families/qwen3_5/xpu*.zig,zig/src/xpu/exl3.zig,ggml.zig,zig/kernels/xpu/{qwen_*,exl3*,ggml_*},zig/src/native/xpu_qwen.zig(the Qwen entry of the lane host),zig/tests/xpu/{qwen_*,exl3,ggml*}.zig,tools/zig/{xpu_needle.py,xpu_spec_identity.sh,xpu_fixtures_ggml.py,xpu_fixtures_exl3.py}, the Qwen section ofdocs/recipes/xpu-arc.md, andLICENSES/plus the notices for ggml/llama.cpp,ExLlamaV3 and MLX.
Not in this PR: GGUF through the native server; drafts on the server; other Qwen3.5 sizes (refused before loading); 16 GB cards.
Gates (CONTRIBUTING.md)
zig/kernels/xpu/ggml_tables.h, 632 lines of a generated lookup table (generatortools/gen_ggml_tables.py). The exemption is the question in #522; a split of the table under 600 lines is ready if it is refused.zig build -Dxpu,native-xpu,xpu-tests,zig build test: 89 of 90 pass; the failure is upstream'sdirect_iotest, which fails the same way on an unmodified 1.0.2 checkout on this machine.xpu_spec_identity.sh, token for token, 18 runs of the script, all PASS, on the exported tree. Short prompts (7 prompts, 299 tokens, k=4): EXL3 mtp, GGUF IQ3_S mtp, MLX mtp with the EXL3 head, MLX copy drafts, EXL3 auto. Long prompts (2048-row windows on both runs, k=3 and k=4, 80 tokens): copy, auto and mtp drafters on EXL3 and GGUF IQ3_S at 8,192 and 16,384 tokens (12 runs), and mtp k=4 (64 tokens) at 32,768 tokens on EXL3, GGUF IQ3_S and MLX with the EXL3 head. The copy-drafter index agrees with the full scan on 9,000 random calls (qwen_copyidx).qwen_rows: 2, 3, 4, 8, 16 rows plus pending-then-commit, 0 differing values, for MLX, EXL3 and Q5_K_XL. The delta-rule window kernel at 1, 2 and 4 rows a sub-group is bit-identical at 16 to 2048 rows (qwen_gdnrows).qwen_slot: 0 of 248,320 logits differ.qwen_fp64, MLX 4-bit, real weights, decode / 8-row window / 64-row prefill kernels: all 90 rows within the bounds (history below). EXL3 and the GGUF types have their own oracle tests (below).--prefill 512, bf16 KV, first 64 chunks only, not the full corpus, measured with a build bit-identical to this export: 16,320 of 16,320 rows identical to the prototype's, max log-prob difference 0, 0 same-top flips): recipe C 0.0437, MLX 4-bit 0.0450, EXL3 3.00 bpw 0.0436. Full-corpus figures from the engine's KL harness: recipe C 0.0497, MLX 4-bit 0.0536, EXL3 3.00 bpw 0.0645.test-goldenTHIRD_PARTY_NOTICES.mdsection for the Intel GPU engine; license texts for ggml, ExLlamaV3 and MLX inLICENSES/. The fixtures: see below.Exactness details
qwen_seam(640 tokens of real text): chunks of 64 and 128 are byte-identical; chunks of 512 differ from them. Below 256 rows the EXL3 projections use the decode kernels in 16-row steps; from 256 rows they use the matrix-engineprefill GEMM, and the fp16 gate projections switch kernels at the same size. In a scratch build with both removed, chunks of 64, 128 and 512 agree bit for bit, so these two switches are the whole cause. They are kept, because unifying them
would change the bits of every prompt chunk above 16 rows. Size: decode logits after the prompt differ between chunks of 128 and 512 by at most 2.07e-1 (largest logit 22.8), top-1 equal in 7 of 8 steps. The test fails on any chunk-size
difference unless told where a path change is expected (
differ=256). It is not new: the earlier clean-clone runs show EXL3 chunks of 64 and 512 differing in the same way.and 0.27 on EXL3 (7 of 8).
qwen_fp64failed four one-row decode rows (gdn.out, gdn.z, attn.q, mlp.down) under a bound that divided each column's error by that column's own maximum, which is too strict for a single-row checkof a column whose exact value cancels to about 4e-5: its error of 2e-6 to 4e-6 is 3e-8 to 5e-8 of the sum of the absolute terms, below one fp32 rounding unit. The test now divides by the larger of the column's maximum and 1e-4 of the class maximum
and still prints the old figure beside the new one. With that rule the aggregate error is 1.3e-3 to 2.0e-3 in all 90 rows; the worst column is 3.9e-3 for window and prefill kernels and 1.6e-2 for one-row decode, against a bound of 2e-2; error relative to the term sum is at most 3.5e-4.
The Nemotron test of part 1 keeps the rule it was published with; its table was rerun and is unchanged.
tf-xpu-exl3-test: 32 cases, 27 synthetic trellises and five rows of real tensors (fixtures fromtools/zig/xpu_fixtures_exl3.py:synthneeds no checkpoint,realreadsTF_EXL3_DIR; nothing from weights is committed, and without the real slices the five real-tensor cases do not run), decode bit-exact where checked, worst relative error 5.7e-7, row-invariance differences 0.tf-xpu-ggml-test: all 14 block-type fixtures including the new Q8_0 and IQ4_NL: dequantization and quantized-activation differences against libggml 0; matvec within 3.4e-7 of float64 (fp32 output) and 3.5e-3 (bf16); window rows 0 differences (m = 2..8);chunked prefill 0 differences.
4d63633f4fb0for both), and in themtp-testat 8,000 tokens (k=3, identical, 100% of the drafts accepted). The two runs must use the same prompt windows: a plain run that prefills in 16-row windows and a drafted run that prefills in 2048-row windows are different prefill paths (the prefill-GEMM seam above) and can differ at near-tie tokens. At 120,000 tokens on EXL3 the drafted run decoded 33.6 tok/s against 21.6 tok/s plain on this prompt (64 tokens, 40 of 86 drafts accepted, identical digestded18707a820). On a GGUF mix (recipe D19pm, the same family as recipe C and also unpublished) with the prototype build (arc-engine main 1c7fec1, ReleaseFast, q8 KV, needle prompts at depth 50%, 80 tokens, k=3, plain and drafted runs from the same prefill path; not rerun on this tree), decode in tok/s for plain / MTP drafts / the auto drafter: 20.94 / 41.40 / 44.94 at 65,536 tokens; 19.93 / 37.01 / 38.40 at 98,304; 19.29 / 35.88 / 31.62 at 122,880. MTP drafts are 1.86x to 1.98x faster than plain at every depth measured there; the auto drafter's figure at 122,880 used the full-history copy scan that the copy-drafter index of this PR replaces (that run is being repeated). The drafted prefill costs more than the plain one there too (plain / drafted: 57.6 / 137.0 s at 65K, 95.6 / 222.1 s at 98K, 128.3 / 293.2 s at 123K). There is no automatic fallback to plain decoding; drafts only change speed, never the tokens. No claim is made that drafts win or lose at every long length, and nothing above 32K is gated for identity.Digests
The CLI prints the SHA-256 of the generated ids written as a JSON list (
[1, 2, 3]); the server'stoken_shais the SHA-256 of the ids joined by commas (1,2,3). The same tokens give different digests.tools/zig/xpu_token_sha.py REPORT.json(part 2) prints both. For the MLX p0 run the CLI digest is67f96813a817and the server'sa4fda3139fa0; for EXL3f579aeec19baand294fb1554dd4; the server's completions for the same prompt hash tothose values, so the two produce the same tokens (KV q8 and bf16 gave identical tokens for this prompt on both).
Receipts
Intel Arc Pro B70, Linux 7.0.0 (
xe), Zig 0.17.0, host with 62.2 GiB RAM and 24 threads. Checkpoints:TensorFold/Qwen3.8-27B-MLX-4bitrevision22d8d538154e0e5b6f8dcb1fcc74a60a0798104c;turboderp/Qwen3.8-27B-exl3branch3.00bpwrevision6fe61ad620abfe97c5b49f9722c2bceeea4ccc28;Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp.gguffromISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF;Qwen3.8-27B-UD-Q5_K_XL.gguf, the Unsloth UD-Q5_K_XL GGUF as downloaded on 2026-09-12 (20,876,938,144 bytes, sha2568601193d3d5760c37fb8ce1b43afebc69df5fb24e1fbc5a547c32e2200305276; repository and revision not recorded). KV q8 unless stated.Recipe C, used for the KV and long-context rows, is not a published checkpoint: an importance-weighted
llama-quantizemix of the BF16 GGUF (12.56 GB; importance matrix and per-tensor type overrides from a sensitivity sweep, described in the recipe). Its per-tensor type table is not shipped, so it cannot be rebuilt from this tree.ARC_PF=old/ARC_PF=1: MLX 6.69 / 7.08 / 8.41 at 8k and 27.9 / 34.7 / 44.7 at 32k;EXL3 7.68 / 8.08 / 9.48 at 8k and 31.9 / not run / 48.8 at 32k.
F16_NOPF=1on EXL3: 7.94 s at 8k, 33.0 s at 32k.Timed cells of that probe (depth-50 prompts, 64 generated tokens, each run alone, measured with a prototype build whose optimize mode is being confirmed, not on this tree), prefill s / decode tok/s / peak GB, bf16 | q8 | q4: recipe C at 32K 25.6 / 30.45 / 15.98 | 25.5 / 32.07 / 15.09 | 25.5 / 32.92 / 14.55;
at 120K 130.0 / 22.57 / 21.88 | 127.4 / 26.22 / 18.23 | 126.5 / 28.35 / 16.21; MLX 4-bit at 32K 25.7 / 26.88 / 18.64 | 25.4 / 28.63 / 17.75 | 25.4 / 29.33 / 17.21; at 120K 130.0 / 20.57 / 24.54 | 127.1 / 24.01 / 20.89 | 126.2 / 25.76 / 18.87.
Prefill barely depends on the KV format; decode is faster with a smaller cache and more so at depth (recipe C at 120K: q8 +16%, q4 +26% over bf16). llama.cpp's Vulkan backend shows the opposite (q8_0 KV halves its decode at 32K depth),
so this is a property of each engine's kernels, not of KV quantization itself.
q4 gives +0.0024 (+3.8%) at 16K and +0.0032 (+7.4%) at 32K, same-top-token 0.2 points lower, and saves 0.8 to 1.55 GB. q4 is an opt-in mode for pushing context, not a free saving.
-ngl 99 -fa,-c 123392 -np 1, f16 KV, prompt cache off, needle-style prompts rather than the arrays above): on recipe C cold prefill 5.28 s at 2k, 60.75 s at 16k, 537.9 s at 64k, 1,654.8 s at 120k, decode after the prompt 13.56 to 7.18 tok/s; on the stock IQ3_S file5.25 s at 2k, 60.3 s at 16k, 536.1 s at 64k, 1,651.2 s at 120k, decode 11.39 to 6.48 tok/s. No engine number on prompts of those lengths has been measured on this tree (an earlier figure came from a prototype build whose optimize mode is not confirmed, and is not quoted).
The full tables are in the recipe. This is an order-of-magnitude comparison, not a matched benchmark. There is no previous release for this backend.
Reproduce
zig builddefaults to-Doptimize=fast(ReleaseFast); every number in this description was measured with it, and a debug build is much slower. (Thetensorfold-nativebinary keeps its HTTP side at ReleaseSafe, as upstream does.)tools/zig/xpu_token_arrays.pywrites the cold-prefill arrays (use the Qwen checkpoint's tokenizer). Do not wrap a script that takes the guard itself (xpu_spec_identity.sh,xpu_needle.py, the comparison scripts whenTF_GPU_LOCKis set) inxpu_guard.sh.Fixtures: the ggml and EXL3 real-tensor fixtures hold raw quantized rows of real tensors and their libggml dequantization. They are generated locally by
xpu_fixtures_ggml.pyand are not in the tree; the tests skip cleanly without them.The picks are rows of
output.weightandblk.17.ffn_gate.weightof the Q5_K_XL file for Q8_0 and IQ4_NL and the equivalent tensors of the other files for the older types; the generator names each tensor and row range.Open questions
ggml_tables.h(see Proposal: an Intel GPU (Level Zero) backend, starting with Nemotron 3.5 Lightning on Intel Arc #522)..gguffile; today it resolves directories only.