Skip to content

Pascal GP100: a Q8_0 decode GEMV (STRATA_Q8_SM60=1, opt-in) and IQ3_XXS / IQ3_S tables in shared memory - #1424

Closed
ruibeikaa wants to merge 5 commits into
Niko1221:mainfrom
ruibeikaa:pascal-q8-decode-sm60
Closed

ruibeikaa wants to merge 5 commits into
Niko1221:mainfrom
ruibeikaa:pascal-q8-decode-sm60

Conversation

@ruibeikaa

@ruibeikaa ruibeikaa commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Title

Pascal GP100: a Q8_0 decode GEMV (STRATA_Q8_SM60=1, opt-in) and IQ3_XXS / IQ3_S tables in shared memory

Summary

Two sm_60 changes for Pascal GP100 (the chip from #124), measured on a PH402 SKU 200: two boards, four GP100 dies of
48 SMs and 32 GB HBM2 each.

GP100 is the one Pascal chip without __dp4a, and on a GP100 die the decode window's dense projections read their
weights at 40-80 GB/s of ~730: the K- and i-quant blocks are decoded one int at a time. With STRATA_Q8_SM60=1 and a
shard whose dense projections and head are Q8_0, decode goes from 28.3 to 38.9 tok/s (+37%) on Flash-Next
IQ3_XXS; prompt reads are unchanged. Everything is off by default except the IQ3_XXS and IQ3_S tables, which are
bitwise the same output and on by default for compute capability 6.0 (GP100) only; the IQ3_S ones add +3.2%
decode
on Swift 1.5 IQ3_XXS.

Flash-Next IQ3_XXS, 32K, decode tok/s ~10.7K-token document the same, cached short reasoning code mean
GSQ-RCO shard, exact kernels 28.8 29.4 28.4 26.8 28.3
Q8_0 dense shard, STRATA_Q8_PACKED=1 27.5 31.0 34.4 31.8 31.1
Q8_0 dense shard, STRATA_Q8_SM60=1 37.5 38.4 41.2 38.5 38.9

GPU time per verify window (STRATA_VERIFY_PROFILE): dense projections 19.2 -> 11.2 ms, head 4.25 -> 2.1 ms; the
draft policy then picks longer windows (2.10 -> 2.31 tokens per window). Prompt reads: 437-438 tok/s at 10.7K in
every row.

Conditions: application clocks locked at 1050 MHz, Windows 11, driver 581.80 (TCC), CUDA 12.9, engine 0.1.40.2 (the
branch is rebased on 0.1.40.3, which touches none of these files), --layer-split 12,24,36 --prefill 2048, greedy,
256 new tokens, medians of 2-3 interleaved engine starts, the engine kept on its own logical CPUs (Extra Notes).

What changed

  • src/kernels/cuda/q8_sm60.cuh (new): the GP100 Q8_0 GEMV. A warp (half / quarter warp for rows under 2048 / 1024
    wide) per row; 16-byte weight loads through the read-only cache; the window's q8_1 activations copied once per
    workgroup into shared memory as an int8 plane plus float scales, so every activation read is a 16-byte shared load;
    STRATA_DP4A (vmad on sm_60); a shuffle sum with no barrier per row; persistent workgroups, 4 per SM
    (STRATA_Q8_SM60_BPS). Shapes needing more than 48 KiB of shared memory (a 6144-wide row with 7-8 columns) are
    declined and keep the usual kernel. Also a device-side GGUF-block -> planes repack for the head.

  • src/kernels/cuda/native_mmvq.cu, include/strata/kernels/native_mmvq.hpp: STRATA_Q8_SM60=1 makes every Q8_0
    matrix eligible for the packed planes and dispatches registered matrices to the new kernel (any ncols it accepts,
    independent of the exact-layout requirement); native_q8_0_sm60_pack / native_q8_0_sm60_release own the head's
    packed copy. Not compiled into HIP builds. Without the variable nothing changes.

  • src/core/native_head.cpp: a Q8_0 head is packed when STRATA_Q8_SM60=1 (the MTP draft head reads the same matrix).

  • src/kernels/cuda/iq_kernels.cu: IQ3_XXS and IQ3_S join the types whose codebook the grouped expert kernels stage
    in shared memory, each with sign-mask tables applied as (g ^ mask) + (mask & 0x01010101) (two 128-word tables for
    IQ3_XXS's 7-bit sign fields, two 256-word tables for IQ3_S's sign bytes; no codebook byte is 0, so a negated byte
    never carries). IQ3_XXS's are ported from
    shinbunbun/llama-cpp-p100-patches (MIT), patches 29
    mmvq-iq3xxs-grid-smem and 30 mmvq-ksigns-smem; IQ3_S's are the same idea - IQ3_S was the one gate/up format
    whose codebook these kernels still read from global memory. Bitwise the same output (iq_multi_parity and
    native_grouped_parity on a GP100 die; identical greedy replies, and the same drafts accepted in the A/B below).
    IQ3_XXS's alone: within noise on GP100 (+0-4%). IQ3_S's: a verify window's layer of real IQ3_S experts (20 experts,
    24 entries) on one die 517 -> 393 us with a Q2_0 down projection and 492 -> 369 us with IQ4_NL; Swift 1.5 IQ3_XXS
    (13 of its 48 layers IQ3_S), 1M context, otherwise the conditions below, decode tok/s:

    Swift 1.5 IQ3_XXS, STRATA_Q8_SM60=1 ~10.7K-token document the same, cached short reasoning code mean
    IQ3_XXS tables 36.0 36.3 43.1 41.6 39.3
    IQ3_XXS + IQ3_S tables 37.2 37.4 44.5 43.0 40.5

    Medians of three interleaved engine starts per build; on every prompt each of the three runs with both tables was
    faster than each run without. The staging only pays where these kernels are decode-bound: on an RTX 3090 Ti (sm_86),
    forced on, the same IQ3_S layers took 3-13% longer (71-76 -> 75-84 us; there they already read at 490-665 GB/s) and
    IQ3_XXS was unchanged. So the default is compute capability 6.0 only (GP100 has no __dp4a; 6.1 cards do and are
    unmeasured); STRATA_IQ_STAGE_TABLES=1|0 forces it on or off on any card.

  • tools/q8_dense_gguf.py (new): the GSQ-RCO packs have no Q8_0 dense matrices, so this writes a copy of the model
    whose natively served projections and head are Q8_0 (Flash-Next IQ3_XXS: 301 tensors, +3.5 GiB, requantization
    error at most 0.4% of a tensor's |w|max). Every shard that holds such a matrix is converted - Swift 1.5 keeps 78 of
    them in shard 1 and 223 in shard 2 - and the others are hard-linked (--copy copies). The tensor-info block keeps
    its byte length, so every expert keeps its absolute offset and the existing native pack is reused.

  • tools/q8_sm60_check.cu (new, built by hand like sm60_dp4a_check.cu): 57 shape x column cases (the Flash-Next
    Q8_0 shapes, 1-8 columns) against a CPU double reference, max relative error < 1e-5, then times them.

  • docs/OLDER_GPUS.md: a "Pascal GP100: a Q8_0 decode kernel (opt-in)" section with the table, the conditions and how
    to use it; the P100 row of the support matrix points to it.

Extra Notes

  • Output: STRATA_Q8_SM60 is not bitwise the exact kernels (Q8_0 requantization of the dense matrices and another
    summation order). Greedy replies to five prompts (arithmetic, logic, Chinese, code, a question on a 10K-token
    document) gave the same answers, with wording differences after the first sentence or two.
  • VRAM: the packed copies sit beside the GGUF layout the prompt path reads (Flash-Next: 2.9 GiB of dense projections
    over the stages, 0.6 GiB for the head).
  • Two things mattered as much on this card, and are written up in the docs section: its default application clock is
    759 MHz and a layer split leaves each die idle most of a window, so the dies decode below 1050 MHz unless locked
    (nvidia-smi -ac 715,1050; 25.0 -> 29.1 tok/s at 10.7K context); and a second engine on the same PC that pins its
    threads to the same logical CPUs halved this one's decode while it ran - keeping them on different logical CPUs
    (SMT siblings) avoided it.
  • Not measured on compute capability 6.1 (P40, P4, GTX 10 series), which has __dp4a. Both changes build for sm_61,
    and where __dp4a exists the Q8_0 kernel's dot is the hardware instruction (tools/q8_sm60_check.cu passes on an
    RTX 3090 Ti). Numbers from a 6.1 card with STRATA_Q8_SM60=1 (and a Q8_0 dense shard) or with
    STRATA_IQ_STAGE_TABLES=1 would show whether either helps there.
  • Built with CUDA 12.9 and MSVC 2022 (-DSTRATA_EXPERIMENTAL_SM60=ON -DCMAKE_CUDA_ARCHITECTURES=60); also for sm_86,
    where iq_multi_parity and native_grouped_parity pass with the tables on and off. Not built for HIP; the new kernel
    only runs with the variable set, and HIP builds leave it out.

@ruibeikaa ruibeikaa changed the title Pascal GP100: a Q8_0 decode GEMV (STRATA_Q8_SM60=1, opt-in) and IQ3_XXS tables in shared memory Pascal GP100: a Q8_0 decode GEMV (STRATA_Q8_SM60=1, opt-in) and IQ3_XXS / IQ3_S tables in shared memory Oct 7, 2026
…ped expert kernels)

Ported from shinbunbun/llama-cpp-p100-patches (MIT), patches 29 `mmvq-iq3xxs-grid-smem` and 30
`mmvq-ksigns-smem`: the grouped expert kernels stage the 256-word iq3xxs_grid and two 128-word tables of per-byte
sign masks once per block, and apply a mask as (g ^ mask) + (mask & 0x01010101) instead of the per-lookup
unpack_ksigns / __vcmpne4 / __vsub4 chain (iq3xxs_grid's bytes are all >= 4, so no carry crosses a byte).

Bitwise the same output: checked end to end on a PH402 (GP100), greedy, three prompts x three interleaved rounds,
identical replies with and without. Measured only on GP100, where it is within noise (+0-4% decode), so it is the
default on compute capability 6.x only; STRATA_IQ_STAGE_GRID18=1 / 0 forces it on / off on any card.
GP100 has no __dp4a, and on a GP100 die the verify window's dense projections read their weights at 40-80 GB/s
of ~730. With STRATA_Q8_SM60=1 every Q8_0 dense matrix (any shape) and the head get the packed planes of
STRATA_Q8_PACKED and run src/kernels/cuda/q8_sm60.cuh: a warp (half / quarter warp for narrow rows) per row,
16-byte weight loads through the read-only cache, the window's q8_1 activations copied once per workgroup into
shared memory as an int8 plane plus float scales, STRATA_DP4A (vmad on sm_60), a shuffle sum with no barrier per
row, persistent workgroups (4 per SM, STRATA_Q8_SM60_BPS). Shapes it declines (> 48 KiB of shared memory: a
6144-wide row with 7-8 columns) keep the usual kernel. Not bitwise the exact kernels (summation order); the
packed copies cost their size in VRAM (Flash-Next: 2.9 GiB of dense projections over the stages, 0.6 GiB head).

The GSQ-RCO packs have no Q8_0 dense matrices: tools/q8_dense_gguf.py writes a shard 1 whose natively served
projections and head are Q8_0, keeping the tensor-info block's length so every expert keeps its offset (the native
pack is reused) and hard-linking the other shards beside it.

PH402 SKU 200 (four GP100 dies), clocks locked at 1050 MHz, Flash-Next IQ3_XXS, 32K, layer split 12,24,36,
--prefill 2048, greedy, decode tok/s (medians, interleaved starts): GSQ-RCO shard 28.3, Q8_0 shard with
STRATA_Q8_PACKED 31.1, Q8_0 shard with STRATA_Q8_SM60 38.9 (+37%); per window dense 19.2 -> 11.2 ms, head
4.25 -> 2.1 ms; prompt reads unchanged. tools/q8_sm60_check.cu: 57 shape x column cases against a CPU reference,
max relative error < 1e-5. docs/OLDER_GPUS.md has the table, the conditions and how to use it.
The tool converted only the shard it was given and hard-linked the others, which is enough for Flash-Next (every
natively served projection and the head are in shard 1) but not for Swift 1.5 IQ3_XXS, which keeps 78 of them in
shard 1 and 223 in shard 2: one run left three quarters of them in their K- / i-quants, silently, and the kernel
only runs Q8_0 matrices. Now any shard of the model may be named; every shard that holds such a matrix is written
as before (same tensor-info length, Q8_0 blocks appended, offsets checked) and the others are hard-linked.

Checked: Swift 1.5 IQ3_XXS in one run gives both shards byte-identical to two single-shard runs (301 tensors);
Flash-Next IQ3_XXS converts 301 tensors in shard 1 and links shard 2.
IQ3_S was the one i-quant gate/up format whose codebook the grouped expert
kernels still read from global memory (IQ2_XXS / IQ2_XS / IQ2_S stage theirs
everywhere, IQ3_XXS on 6.x since the first commit of this branch). Its 512
codebook words are now staged per block with two 256-word tables of byte
masks - IQ3_S keeps one explicit sign byte per 8 values, so the tables hold
the masks of a sign byte's bits 0-3 and 4-7 - applied as
(g ^ mask) + (mask & 0x01010101): every iq3s_grid byte is odd, so a negated
byte never carries into the next. Bitwise the same output (iq_multi_parity
and native_grouped_parity on a GP100 die, with and without
STRATA_IQ_STAGE_GRID_MMVQ=1).

One GP100 die, a verify window's layer of real Swift 1.5 IQ3_S experts
(20 experts, 24 entries): 517 -> 393 us with Q2_0 down, 492 -> 369 us with
IQ4_NL down. Swift 1.5 IQ3_XXS (13 of 48 layers IQ3_S), four GP100 dies,
1M context, decode tok/s, medians of three interleaved engine starts:
36.0 -> 37.2 (10.7K document), 36.3 -> 37.4 (cached), 43.1 -> 44.5
(reasoning), 41.6 -> 43.0 (code), with the same drafts accepted.

The switch covers both formats and is renamed accordingly:
stage_tables_on(), STRATA_IQ_STAGE_TABLES=1|0 (was STRATA_IQ_STAGE_GRID18).
Still the default on compute capability 6.x only.
The staged IQ3_XXS / IQ3_S tables trade a per-block copy and barrier for
cheaper lookups, which pays only where the grouped expert kernels are
decode-bound. On GP100 (6.0: no __dp4a, 70-140 GB/s) it does; on an RTX 3090
Ti (sm_86), forced on with STRATA_IQ_STAGE_TABLES=1, the same real Swift 1.5
IQ3_S layers (20 experts, 24 entries) took 3-13% longer (71-76 -> 75-84 us;
those kernels read at 490-665 GB/s there) and IQ3_XXS was unchanged, the
output bitwise the same. 6.1 cards (P40, GTX 10) have __dp4a and are
unmeasured, so the default now asks for 6.0 rather than any 6.x;
STRATA_IQ_STAGE_TABLES=1 still turns it on anywhere.

docs/OLDER_GPUS.md says so, and that neither STRATA_Q8_SM60 nor the tables
is measured on 6.1: both build for sm_61, and tools/q8_sm60_check.cu passes
on an RTX 3090 Ti, where the kernel's dot is the hardware __dp4a.
@Niko1221

Copy link
Copy Markdown
Owner

Thank you for this. We ported it and gated it on the cards we have (RTX 3060, RTX A4000, Tesla P100, R9700, Arc A750), and found the group it came in with slowed default decode on an sm_86 card by about 3-4% on a long-context prompt after the merge, which we could not yet tie to one commit. A default must not get slower, so the Volta/Pascal group moves to 0.1.46, where we find the cause first. Your commits stay credited to you on our branch f-145-pp-volta. Nothing was declined for being wrong.

Niko1221 added a commit that referenced this pull request Oct 19, 2026
…e default on cc 6.0: no end-to-end gain on a Tesla P100 (#1424)
@Niko1221

Copy link
Copy Markdown
Owner

Thanks for the pull request.

Your change is already in main for 0.2.0-beta, with your credit, so there is nothing left for you to do here. We're closing it because we're resetting the tracker for 0.2.0-beta: there are more than 1,300 open issues and pull requests, and we're closing everything that isn't being worked on right now.

If part of your work did not make it in and you want it considered, open a new pull request on top of main that follows CONTRIBUTING.md, or comment /reopen after fixing the title area prefix and the template.

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