Repository navigation
perf(hip): prompt GEMMs write the activation type, not fp32 and a cast - #1685
Conversation
|
Thanks @AbrahamCete, the round test and its truncating control are what we'd want. Two things before it lands:
|
850bb4b to
5986bf6
Compare
|
Thanks @ashhart. The branch is rebased onto c3c3bd0 (both import lines stay), and the description's receipt now has the numbers against current main. On the rebased head 5986bf6 against c3c3bd0, gfx1151:
I don't have a gfx1100 or gfx1201 card. I can open an ATC exchange test request for the fp16 path if that helps. |
A prompt projection wrote its fp32 sum and then ran tf_cast to round it to the activation type; nothing else read the fp32 buffer. The prompt tiles (matrix, gemm, the K-parallel tiles and the block reference) now store through GemmOut<T>, which rounds into out16 when it is set, exactly as tf_cast rounds (narrow<T> is float_to_bf16, NaN to 0x7fc0), and affine() asks a prompt for out16 the way decode already did. The plain prompt entries declare `rounds = .always`; routed ones keep fp32 for the combine. A kernel test runs every prompt tile on 12 shapes with out16 set and requires tf_cast's bytes of the same tile's fp32 sum, with out untouched. Qwen3.8-27B on gfx1151, a prompt pass of 2048 tokens: 8.50 to 8.11 s; 8192 tokens: 35.29 to 33.74 s.
5986bf6 to
e2b0943
Compare
|
Thank you @AbrahamCete. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes. |
|
Thank you @ashhart! |
|
Thanks @AbrahamCete, that answers both: the rebase keeps #1648's import, and the round and order tests, the 27B logits and the cold-prefill pair on the rebased head are all against current main. 2-3% off cold prompts with the same bytes is a clean win. We'll land it, with gfx1100 and gfx1201 listed as not run. No need for an exchange request for the fp16 path. If someone with one of those cards runs |
|
Thanks @AbrahamCete! HIP prompt projections that round to the activation type in their tile are in 1.2.1: https://github.com/ashhart/TensorFold/releases/tag/v1.2.1 |
What this changes
A prompt projection on HIP wrote its fp32 sum into an arena buffer, then ran
tf_castto round it to the activation type. Nothing else read the fp32 buffer (affine()inzig/src/hip/ops/project.zig). On Qwen3.8-27B that is 496 extra launches in a 2048-token prompt pass, about 4% of its kernel time.The changes:
GemmOut<T>. These areaffine_wmma_gemm,affine_gemm_block,affine_gemm_kp, and theaffine_dot2_blockreference.out16is set,GemmOut<T>rounds into it withnarrow<T>, which isfloat_to_bf16/__float2half_rnbit for bit, NaN to 0x7fc0 included.F32Outdid.rounds = .always, soaffine()asks a prompt forout16the way decode already does. The cast launch is gone.Kernels.runare unchanged.The bytes are unchanged: the tile hands
narrow<T>the same fp32 value thattf_castread back. There is no new kernel symbol.This is independent of #1648 and #1663. It conflicts trivially in two places:
prefill()entry list;halfinaffine(). hip: Qwen3.8-Flash-Next on gfx1151, a reference branch for review (to be split) #1667 makes that change for itsdensetile, and this PR makes it for every prompt tile.Receipt
Environment: Ryzen AI Max+ 395 (gfx1151), Ubuntu 26.04, Zig 0.17.0, Ubuntu clang 21 hipcc (ROCm 7.13 from TheRock for the contract limits), checkpoints Qwen3.8-27B-MLX-4bit and Qwen3.6-35B-A3B-MLX-4bit-MTP.
mainea3567b (before) with this change on top of it (after).kernel_tests.zig.mainnow includes perf(hip): band the prefill WMMA GEMM's blocks so row blocks share weights #1648 and perf(hip): keep the matrix tile's inner loop free of selects and divisions #1663, which change the same prompt GEMMs, so I measured again on the rebased tip against c3c3bd0:main, in bf16 and with--f32.ttft_s, runs alternated B1 C1 B2 C2,token_shaidentical at every size: 2048 5.52 -> 5.38 s (-2.5%), 8192 23.16 -> 22.53 s (-2.8%), 32768 109.40 -> 107.22 s (-2.0%).hip-kernel-test108 passed, 1 skipped, 0 failed (main: 107 passed, 1 skipped; the skip ismain's Flash-Next MTP wide-norm test). Kernel contractSUMMARY gfx1151 50 kernels: 50 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 126 limit problems, limits 220 andlean_check424, each the same asmain.check: 27B--only invariants42/0/5, 35B-A3B 46/0/1.Exactness:
kernel_tests/affine/round.zig. Every prompt entry that takes each of 12 shapes runs with bothoutandout16valid, 63 launches in all:tf_cast's bytes of the same entry's fp32 sum inout16, withoutuntouched.round 1 row: mlx.project.prefill.kp0 with out16 writes other bytes than tf_cast of its fp32 sum) and passes after.tf-qwen35-test logits) are byte-identical before and after, in bf16 and with--f32: 27B on 300 and 2048 prompt tokens, and 35B on 300.GemmOutfails the new test and two existing affine error checks, and changes the 27B logits.token_shais identical at every size (2kfaceb02af600, 8k1ebfd15d62c7, 32kc8dfa0175e40, 64kd92b23432e24).tools/bench_concurrent.py --levels 1,4 --tokens 256 --reps 3 --temperatures 0 --alone --serial, run twice on each side:token_shais the same before and after: code2b28aa2108de, chata06fab348fbe.tf-qwen35-test check: 27B--only invariants42/0/5 and 35B-A3B 46/0/1, the same on both trees.Decode speed before and after: unchanged, since decode already rounded in its tiles.
--context 4096 --parallel 4 --prompt-cache-gib 0. Runs alternated B1 C1 B2 C2.tools/bench_openai.py --tokens 256 --reps 3 --temperatures 0, median tok/s:fibonacci-raw: 13.21, 13.29 before; 13.25, 13.27 after.gpu-chat-no-think: 13.52, 13.58 before; 13.49, 13.61 after.bench_concurrentaggregate at 4 streams: code 43.8-43.9, chat 43.2-43.4, on both sides.Prompt speed before and after: cold first request
ttft_sfromtools/bench_cache.py --require-cold --tokens 16.tensorfold-native serve --context 66560 --parallel 1 --prompt-cache-gib 0 --snapshot-dir none --max-snapshots 0, with a fresh server for each run.cached_tokens = 0, and every run qualified.tf-qwen35-test prefill <27B>: 2048 tokens 8.50 to 8.11 s, and 8192 tokens 35.29 to 33.74 s.bench_cacheJSON.Tests run:
hip-kernel-test(binary run directly): 107 passed, 0 failed, 0 skipped (mainhas 106; the new one isaffine.round).test-qwen35-hip-host,hip-host-test,check-native-hip-host: exit 0.SUMMARY gfx1151 49 kernels: 49 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 126 limit problems, the same onmain. The only line that differs between runs is where the planted atomic-add control first differs.tools/zig/lean_check.py: 425 problems onmainand on this branch, none new.zig fmt --checkclean on the changed Zig files.Not run, and why:
__float2half_rnon both sides, which I checked by reading the code.affine()is their prompt path too, and the change keeps the bytes for any tile, as the kernel test and the logits show here.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (Bytes unchanged: the round test, the prompt logits, servedtoken_sha, andbench_concurrentalone and serial equal;tf-qwen35-test checkmatchesmain.)SUMMARYline is in the receipt. (No new symbol; the changed prompt tiles have no records; SUMMARY unchanged.)mainea3567b; 2.0-2.8% faster thanmainc3c3bd0 after the rebase.)