Skip to content

sycl: PQ2_0/PTQ1_0 XMX kernels from TernSYCL, PTQ1_0 kept at 1.75 bpw - #327

Open
kiljoy001 wants to merge 2 commits into
PrismML-Eng:prismfrom
kiljoy001:sycl-ternary-xmx-ternsycl
Open

kiljoy001 wants to merge 2 commits into
PrismML-Eng:prismfrom
kiljoy001:sycl-ternary-xmx-ternsycl

Conversation

@kiljoy001

@kiljoy001 kiljoy001 commented Oct 7, 2026 •

Copy link
Copy Markdown

Overview

Replaces the ESIMD kernels of the PQ2_0/PTQ1_0 XMX path from #294 with SIMT SYCL kernels ported from Intel's TernSYCL (int2_via_int2_x_int8_dpas, BSD-3-Clause, notice kept in pq2_xmx.cpp), and keeps PTQ1_0 at 1.75 bits in VRAM instead of expanding it to the PQ2_0 layout.

The gate, the xmx_pq2 layout flag, the 16-wide-DPAS capability check, the AOT guard in CMake, the get_rows asserts and every fallback of #294 are unchanged. The diff is pq2_xmx.cpp/.hpp plus the allocation-size rule in ggml-sycl.cpp.

How it works:

  • Kernels. s2 x s8 DPAS and 2D block loads as inline vISA. A mat-vec kernel for up to 8 tokens (K split over sub-groups and reduced in SLM, two 128-steps of loads in flight) and a GEMM with 256 GRF whose tile depends on the batch. When a small batch would leave most of the GPU idle, the GEMM splits K over work-groups and a small kernel adds the slices up.
  • PQ2_0 layout. On first use the weight is rewritten in place: uint32 [K/16][N] s2 codes, then the fp16 scales [K/128][N]. One 2D block load gives a sub-group 16 columns x 128 weights directly in the DPAS operand layout.
  • PTQ1_0 stays at 1.75 bits. It is rewritten to [K/128][7][N] dwords (the 24 qs bytes, then qh and the scale), the same size as the file. The kernels decode the base-3 trits through a 256-entry table in SLM. They decode in byte-major K order (the 5 trits of a byte next to each other), so the 10-bit table entries of consecutive bytes concatenate straight into the s2 dwords; the activation quantizer writes the int8 activations in the same permuted order, so the dot products are unchanged. For large batches each weight block is decoded once per work-group and shared through SLM.
  • Any row count. The XMX layouts pad the rows to 16 (zero columns); the buffer type reserves that room on devices that use the path (a few KB per tensor, e.g. 12 KB for a 151669-row vocab). This replaces the 34-byte-a-block reservation PTQ1_0 needed before.
  • Activations are quantized to int8 with one scale per 128 values, as in sycl: XMX path for PQ2_0 and PTQ1_0 on 16-wide DPAS devices #294.

Two commits: the kernels, and a follow-up that picks PTQ1_0's own tiles for small batches (2-64 tokens) and decodes in registers when no sub-groups share a decoded block. Without it, the in-kernel trit decode made PTQ1_0 3-5% slower than #294 at 8-16 parallel sequences.

Results

Intel Arc Pro B50 (16 GB), oneAPI 2026.1, Level Zero 1.14.37020, Linux 7.0 xe driver, -ngl 99, Ternary Bonsai models. "prism" is the current prism head (6bfcd79, #294 path). All columns measured in the same session.

llama-bench, t/s:

model test prism this PR change
1.7B PQ2_0 pp512 8198 9536 +16%
tg64 185.6 213.9 +15%
4B PQ2_0 pp128 3191 3948 +24%
pp512 3508 4387 +25%
tg64 106.0 121.1 +14%
27B PQ2_0 pp128 457 643 +41%
pp512 491 627 +28%
tg64 19.27 20.66 +7%
27B PTQ1_0 pp128 456 548 +20%
pp512 490 572 +17%
tg64 19.02 22.09 +16%
27B PQ2_0 tg64 @ 16K depth (-fa 1) 13.89 14.73 +6%
27B PTQ1_0 tg64 @ 16K depth (-fa 1) 13.88 15.35 +11%

(pp128 of the 1.7B swings by +-1700 t/s between repetitions on both builds, so it is left out.)

Mat-mul alone, weights rotated through 8 copies so every call reads DRAM, including the activation quantization, us per call:

type shape (K x N) tokens prism this PR
PQ2_0 5120 x 17408 1 120.2 108.7
PQ2_0 17408 x 5120 1 130.4 116.2
PQ2_0 6144 x 5120 1 67.3 43.8
PQ2_0 17408 x 5120 16 173.7 155.5
PQ2_0 5120 x 17408 512 2325 1363
PTQ1_0 5120 x 17408 1 123.6 104.1
PTQ1_0 17408 x 5120 1 129.0 100.5
PTQ1_0 6144 x 5120 1 72.7 58.4
PTQ1_0 17408 x 5120 8 137.3 114.1
PTQ1_0 5120 x 17408 16 141.3 135.1
PTQ1_0 17408 x 5120 16 164.8 121.7
PTQ1_0 5120 x 17408 32 202.5 159.0
PTQ1_0 5120 x 17408 512 2318 1655

Parallel decode (llama-batched-bench -npp 128 -ntg 64), aggregate decode t/s, prism / this PR:

sequences 1.7B PQ2_0 4B PQ2_0 27B PQ2_0 27B PTQ1_0
1 183 / 211 102 / 123 18.8 / 20.6 18.4 / 21.7
2 348 / 404 200 / 233 34.5 / 37.6 34.3 / 38.1
4 639 / 738 380 / 430 55.6 / 59.6 55.8 / 59.9
8 1070 / 1173 669 / 724 80.5 / 81.7 80.1 / 82.2
16 1541 / 1575 988 / 1027 98.2 / 101.8 98.1 / 105.7
32 1868 / 2265 1272 / 1578 111.0 / 117.7 110.8 / 119.7

The 27B's gated delta net layers spend more per sequence in concat and get_rows than in the mat-muls at higher counts; with #326 on top (measured on the first commit) 32 sequences reach 151.9 (PQ2_0) and 140.7 (PTQ1_0) t/s.

PTQ1_0 weights stay at 1.75 bits, where #294 reserves 34 bytes per 28-byte block (about +1.2 GB for the 27B). The mat-muls on the small 1.7B shape (2048 x 6144) are still 5-38% slower than #294 for PTQ1_0 at 2-16 tokens.

Speculative decoding with the multi-token-prediction head (sudoingx/Ternary-Bonsai-2-27B-PTQ1_0-MTP-GGUF, the Qwen MTP head grafted onto the PTQ1_0 file), llama-server, 3 chat prompts (prose, code, bash) x 256 tokens, greedy, thinking off, decode t/s:

prism this PR
target only 18.7 21.8
--spec-type draft-mtp --spec-draft-n-max 1 29.8 (75% accepted) 33.5 (74%)
--spec-type draft-mtp --spec-draft-n-max 2 34.2 (63% accepted) 38.5 (63%)

Correctness

  • test-backend-ops -b SYCL0, run per op over all 130 ops: MUL_MAT 1401/1401, MUL_MAT_ID 1035/1035, MUL_MAT_VEC_FUSION 506/506, GET_ROWS 119/119. The ops that do not pass fully (CONV_2D 2010/2026, FLASH_ATTN_EXT 4070/4073, LIGHTNING_INDEXER 117-120/156 varying between runs, ROLL 1/2; CPY and SET_ROWS abort) do the same on prism.
  • Every PTQ1_0 tile is also checked on real weights: 27B PTQ1_0 perplexity with -ub 6 / 16 / 32 / 64 / 128 / 512 gives 6.2386 / 6.2358 / 6.2394 / 6.2375 / 6.2420 / 6.2347 (+-0.42).
  • With GGML_SYCL_NO_PQ2_XMX defined (the AOT guard for devices without 16-wide DPAS), pq2_xmx.cpp compiles to the stubs.
  • Perplexity (-c 512, 5 chunks of the repository's docs/*.md), prism -> this PR:
model setting prism this PR
1.7B PQ2_0 b1 17.1074 17.1183
1.7B PQ2_0 b512 17.1190 17.1215
4B PQ2_0 b1 12.6491 12.6390
4B PQ2_0 b512 12.6544 12.6511
1.7B PQ2_0 -ngl 0 (op offload) 17.1228 17.1228
4B PQ2_0 -ngl 20 (partial) 12.6494 12.6469
27B PQ2_0 b512 6.2370 6.2356
27B PTQ1_0 b512 6.2370 6.2356

(The two 27B files hold the same ternary weights, so they give the same numbers.)

  • KL divergence against a CPU-only reference (1.7B PQ2_0, -dev none --no-op-offload), prism vs this PR: b1 0.00127 vs 0.00130, b512 0.00127 vs 0.00120; same top token 98.4% vs 98.0% (b1), 97.6% vs 97.8% (b512). Within the error bars.

Additional information

  • Tuning (tiles, K-split thresholds, sub-groups per work-group) was done on the B50 only. Other 16-wide DPAS devices are enabled by capability as before, untested on hardware.
  • The kernels are SIMT SYCL with inline vISA instead of ESIMD. In a JIT build all kernels of pq2_xmx.cpp are compiled together, so a kernel the backend compiler rejects breaks the others too; every tile used here is exercised by the MUL_MAT cases above.
  • PQ2_0 code 3 (+2) has no s2 value, as in sycl: XMX path for PQ2_0 and PTQ1_0 on 16-wide DPAS devices #294; the quantizer never writes it.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES - the code and this description were written by Claude (Anthropic) under my direction; I tested it on my own hardware (Intel Arc Pro B50) and reviewed the changes.

Replace the ESIMD kernels of the PQ2_0/PTQ1_0 XMX path with SIMT SYCL kernels ported from
Intel's TernSYCL (int2_via_int2_x_int8_dpas, BSD-3-Clause): inline-vISA s2 x s8 DPAS and 2D
block I/O, a mat-vec kernel with a K split reduced in SLM for up to 8 tokens, and a GEMM with
256 GRF whose tile depends on the batch size and which splits K over work-groups when a
small batch would leave most of the GPU idle.

- PQ2_0 layout: uint32 [K/16][N] s2 codes (one 2D block load gives a sub-group 16 columns x
  128 weights in the DPAS operand layout), then the fp16 scales [K/128][N].
- PTQ1_0 stays at 1.75 bits: [K/128][7][N] dwords (qs, then qh and the scale). The kernels
  decode the trits through a 256-entry table in SLM, in byte-major K order so the 10-bit
  table entries concatenate into the s2 dwords; the activation is quantized in the same
  permuted order. PTQ1_0 weights no longer expand to 34 bytes a block. For large batches the
  GEMM decodes each weight block once per work-group and shares it through SLM.
- Rows are padded to 16 in the allocation, so any row count works (e.g. 151669-token vocabs).

The gate, the layout flag, the capability check and the fallbacks of ggml-org#294 are unchanged.
…othing is shared

PTQ1_0 decodes its trits in the kernels, so with few tokens the decode, not the weight
reads, set the speed, and the 27B decoded 8 and 16 parallel sequences 3-5% slower than with
the expanded weights of ggml-org#294.

- The GEMM decodes in registers when a work-group has one sub-group row (WG_M == 1), without
  the SLM round trip and barrier that only pay off when sub-groups share the decoded block.
- PTQ1_0 picks its own tiles: 16x16 and 32x16 per sub-group for 5-32 tokens (each decoded
  block feeds 2-4 row blocks), 32x32 with one sub-group row for 33-64 tokens with K >= 6144,
  and the 4-row mat-vec for 2 tokens (the 2-row variant is slower).

Arc Pro B50, Bonsai 2 27B PTQ1_0, llama-batched-bench decode t/s at 8 / 16 / 32 sequences:
77.4 / 93.1 / 110.7 -> 82.2 / 105.7 / 119.7 (prism: 80.1 / 98.1 / 110.8).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant