Skip to content

dequant: coalesced Q8_0 path (four threads per block, 16-byte stores), bit-identical, 7x faster on V100 - #1720

Closed
eelgaev wants to merge 1 commit into
Niko1221:mainfrom
eelgaev:dequant-q8-0-coalesced
Closed

eelgaev wants to merge 1 commit into
Niko1221:mainfrom
eelgaev:dequant-q8-0-coalesced

Conversation

@eelgaev

@eelgaev eelgaev commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

The prompt path dequantizes each dense Q8_0 projection (GDN qkv/gate/out, QSA q/k/v/out, shared expert, hc) into the GEMM scratch before cuBLAS. dequant_kernel runs one thread per 32-value block, so a warp's 2-byte stores land 64 bytes apart: about 70 GB/s of writes on a V100.

dequant_q8_0_kernel gives each block to four threads, 8 values each, written with 16-byte stores: a warp reads 8 consecutive blocks and writes 512 consecutive bytes. The arithmetic per value is unchanged (d * q), so the output is the same bits. It is used for Q8_0 only when the output rows are contiguous (ld == cols) and the pointers are aligned (blocks to 2 bytes, output to 16); everything else (including the padded ld scratch) takes dequant_kernel as before. One file, 32 lines added, nothing else changed.

Bitwise

Every 2-D Q8_0 tensor of Qwen3.8-Flash-Next UD-Q4_K_XL (498 tensors) through dequant_f32, dequant_bf16 and dequant_f16, built from main (fb58e0d) and from this branch: an FNV-1a hash over all outputs is equal for all three output types. dequant_bf16_test covers Q8_0 against the CPU reference as before.

Speed (V100-SXM2-16GB, each call timed on its own)

before after
attn_qkv 10240 x 2560, FP16 0.740 ms (70 GB/s written) 0.106 ms (495 GB/s)
attn_q 12288 x 2560, FP16 0.887 ms 0.126 ms
ssm_out 2560 x 6144, FP16 0.451 ms 0.065 ms
all 498 tensors, FP16 138.5 ms 20.4 ms
all 498 tensors, BF16 140.8 ms 20.5 ms
all 498 tensors, FP32 190.9 ms 35.4 ms

Whole prompts on 4x V100, 4,096-token chunks (where this was first used): 18K 4,537 -> 4,708, 65K 6,540 -> 6,766, 123K 6,783 -> 6,985 tok/s (+3-4%). The kernel is plain CUDA (no architecture-specific instructions), so newer cards take the same path.

The prompt path dequantizes each dense Q8_0 projection (GDN qkv/gate/out,
QSA q/k/v/out, shared expert, hc) into the GEMM scratch before cuBLAS.
dequant_kernel runs one thread per 32-value block, so a warp's 2-byte
stores land 64 bytes apart: about 70 GB/s of writes on a V100.

dequant_q8_0_kernel gives each block to four threads, 8 values each,
written with 16-byte stores: a warp reads 8 consecutive blocks and writes
512 consecutive bytes. The arithmetic per value is unchanged (d * q), so
the output is the same bits. It is used for Q8_0 when the output rows are
contiguous (ld == cols) and the pointers are aligned (blocks to 2 bytes,
output to 16); everything else takes dequant_kernel as before.

V100-SXM2-16GB, every 2-D Q8_0 tensor of Qwen3.8-Flash-Next UD-Q4_K_XL
(498 tensors), each call timed on its own:

  attn_qkv 10240 x 2560, FP16   0.740 -> 0.106 ms   (70 -> 495 GB/s written)
  attn_q   12288 x 2560, FP16   0.887 -> 0.126 ms
  ssm_out   2560 x 6144, FP16   0.451 -> 0.065 ms
  all 498 tensors, FP16        138.5 -> 20.4 ms
  all 498 tensors, BF16        140.8 -> 20.5 ms
  all 498 tensors, FP32        190.9 -> 35.4 ms

An FNV-1a hash over all outputs is equal before and after for FP32, BF16
and FP16. Whole prompts on 4x V100 (where this was first used), 4,096-token
chunks: 18K 4,537 -> 4,708, 65K 6,540 -> 6,766, 123K 6,783 -> 6,985 tok/s.
@G0-0000

G0-0000 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Independent verification from a V100-SXM2-32GB box (Strata v0.1.41 main + your patch applied cleanly — only dequant_bf16.cu, +32/-0):

  • elementwise_parity: 0 failures — 108 row cases, 0 bit mismatches, 0 guard failures; f32→bf16 bulk 0 of 4096 differ. The fast path is bit-identical on our setup.
  • Our model pack is all-Q8_0 dense (503 tensors), so the guard (type==8, ld==cols, 2-byte/16-byte alignment) takes the fast path exactly where it should.
  • We could not resolve an end-to-end prompt gain on this box (cross-shot variance ~18%), so no speed claim from us — but the correctness side is solid: clean apply, no conflicts, bitwise parity green.

Looks good from our side.

txiki739 added a commit to txiki739/Strata3090-5060TI that referenced this pull request Oct 10, 2026
…es (upstream Niko1221#1720, eelgaev)

The prompt path dequantizes every dense Q8_0 projection before cuBLAS; a thread
per block wrote 2-byte values 64 bytes apart.  Same arithmetic (d * q), so the
same bits: tests/dequant_q8_0_identity.cu compares it with the old kernel (a
misaligned output takes that one) for BF16, FP16 and FP32 - identical, and
6x faster on the 3090 (3072 x 2560: 0.19 -> 0.03 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Niko1221

Copy link
Copy Markdown
Owner

Ported with your authorship. dequant_bf16_test passes (10 types, f32/bf16 0 off) on an RTX A4000, and the prompt residual dumps at 4096 and 20000 tokens are identical to the build without it on an RTX 3060 and on the A4000 box. On the RTX 3060 the prompt speed moved by +0.3 to +0.7%, which is within noise there, so we make no speed claim; we could not test the V100 case you describe.

@Niko1221 Niko1221 closed this Oct 10, 2026
eddoursul pushed a commit to eddoursul/Lamina that referenced this pull request Oct 12, 2026
…es (upstream Niko1221#1720, eelgaev)

The prompt path dequantizes every dense Q8_0 projection before cuBLAS; a thread
per block wrote 2-byte values 64 bytes apart.  Same arithmetic (d * q), so the
same bits: tests/dequant_q8_0_identity.cu compares it with the old kernel (a
misaligned output takes that one) for BF16, FP16 and FP32 - identical, and
6x faster on the 3090 (3072 x 2560: 0.19 -> 0.03 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Niko1221 pushed a commit that referenced this pull request Oct 17, 2026
…es on sm_75 (bit-exact; STRATA_TC_SM75=0 keeps the per-group kernel)


The per-group dequant kernel writes 2 bytes every 64 bytes; 0.1.43 coalesced only the Q8_0 path (#1720). On compute
capability 7.5 the BF16 projections of the prompt path take a kernel that writes 16 bytes per thread; the values are
the same (a conversion, no arithmetic), so the output is bit for bit the old kernel's. Other cards are untouched.
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.

3 participants