Repository navigation
[Engine] --pipeline-windows: the front stages' GDN snapshot by buffer rotation instead of a copy per window, same bits (opt-in, STRATA_PIPELINE_SNAP_RING=1) - #3944
Open
ruibeikaa wants to merge 1 commit into
Conversation
…py (opt-in, same bits) Before every speculative window on a front stage, --pipeline-windows 2 copies the stage's GDN state (29.4 MB for a stage of nine GDN layers, Swift 1.5 at --layer-split 12,24,36) on the side stream beside the window, so an undo can restore it. The copy competes with the window for memory bandwidth. With the ring a front stage's state lives in one of its buffers - the session's own and the two snapshot buffers it already holds - and the windows read the one a device word names (Verifier::set_gdn_ring; gdn_conv_l2_multi_ind / gdn_step_norm_multi_ind read the word when they start and then run the verify path's own code). A speculative commit reads the current buffer and writes a free one (gdn_commit_layers_ring: gdn_commit_layers' kernel bodies with a source and a destination buffer, chosen by two more mapped commit words), so the state its window read stays in place for an undo; an undo replays the verified window's commit in place in that buffer; every other commit is in place, as today. snap_take and the undo's restore copy nothing. At the start and end of a pipelined request (after pl_drain) each front stage's state is moved back into its session's buffer if it is elsewhere, so the serial paths, checkpoints and parking never see the ring. No extra VRAM: the buffers are the existing snapshot buffers. Refused (said once, today's copy kept) without the snapshots beside the window (STRATA_PIPELINE_SNAP_OVERLAP=0), with STRATA_QFUSE, STRATA_FUSE_CONVAB, STRATA_COMMIT_BATCH=0, STRATA_GDN_SPLIT=1, STRATA_GDN_COMMIT_SPLIT=0 or STRATA_DBG_GDN=1, on Metal, or when a verifier's graphs were captured before the ring was set. Unset, nothing changes: the verifier's commit words grow by two (the device array keeps its 256-byte size) and the serial commit writes them as 0, 0. gdn_parity section 7: the ring's commit == the in-place commit (n_keep 1..8, out of place and in place, source untouched, neighbouring layer untouched, word set) and the ring's window kernels == today's (T 1..8, outputs from row 0 and T/2), memcmp.
ruibeikaa
force-pushed
the
pipeline-snap-ring
branch
from
October 31, 2026 07:55
b7198b5 to
d927391
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and result
Measured on PH402 SKU 200 boards: four GP100 dies, 32 GB HBM2 each (128 GB in all), every expert resident in VRAM, no CPU share, no PCIe share. The change itself is not GPU-specific: it is host bookkeeping plus GDN kernels shared by every CUDA / HIP build, and any layer split that runs
--pipeline-windows 2has the copy it removes.With
--pipeline-windows 2on a layer split, before every speculative window each front stage copies its GDN state on the side stream beside the window (snap_take), so that a wrong guess can restore it and replay the verified window's commit. For Swift 1.5 at--layer-split 12,24,36that is 29.4 MB per front stage per window (nine GDN layers), 58.8 MB of reads and writes competing with the window for memory bandwidth. On one GP100 die the window graph was 0.13-0.14 ms slower per stage with the copy running beside it (a window is ~6.85 ms per stage there).With
STRATA_PIPELINE_SNAP_RING=1nothing is copied. A front stage's state lives in one of three buffers - its session's own and the two snapshot buffers the loop already allocates - and the window kernels read the one a device word names. A speculative commit reads the current buffer and writes a free one, so the state its window read stays where it was: that buffer is the snapshot. An undo replays the verified window's commit in place in that buffer; every other commit is in place, as today. The bytes every window and commit compute are today's (the same kernel bodies on the same inputs); only the address of the state moves. No VRAM is added.End to end on this branch, four GP100 dies, today's
--pipeline-windows 2: decode +0.8% (57.78 -> 58.24 tok/s, two runs per arm in the order off, on, on, off), every reply and every item of the session state identical, also under forced wrong guesses. On my GP100 stack (engine 0.1.47, with a--pipeline-windows 3loop, details below): decode +0.9..+1.4% (8 prompts token-weighted 60.57 -> 61.15 tok/s, ms per window -0.8..-1.1% on every prompt, 150K context +1.2%), prompt reads unchanged from 8K up, every reply hash identical, and under forced wrong guesses (about 1,230-1,280 undone windows per arm) every stage's state hash identical to the copying loop's - for the 3-deep loop and for today's 2-deep loop, the one main has. Unset (the default) nothing changes.Related discussion
No scope issue: an opt-in switch inside the existing
--pipeline-windowsloop, no default or format change. The loop for three or more stages is #1656's.Approach
src/kernels/cuda/verify_kernels.cu,include/strata/kernels/verify_kernels.hpp:gdn_conv_l2_multi_ind/gdn_step_norm_multi_ind: the verify path's conv and recurrence (no commit, no q8_1 outputs) on the state at*cur + off, read when the kernel starts. The conv callsgdn_conv_l2_bodyunchanged; the recurrence isgdn_step_norm_multi_kernelwith one more template parameter that addsstate = *ind + ind_offat its start.gdn_commit_layers_ring:gdn_commit_layers' two kernel bodies (gdn_step_commit_body_oop: the same statements, the read and the write taking their own base) reading buffertab[sel[0]]and writingtab[sel[1]], after which one thread sets*cur = tab[sel[1]].selis two more words of the verifier's mapped commit words (the device array keeps its 256-byte size), copied in by the commit graph's existingcopy_i32_from_mapped.gdn_ring_supported(): false wheregdn_commit_layersor the window would not take these paths (STRATA_GDN_SPLIT=1,STRATA_GDN_COMMIT_SPLIT=0,STRATA_DBG_GDN=1).src/core/verify.cpp,include/strata/core/verify.hpp:Verifier::set_gdn_ring(only before the verifier's graphs are captured),gdn_ring_ok, the two window kernels and the ring commit wired in when the ring is set, andpl_commit_async(n_keep, err, ring_src, ring_dst). The serialcommit()writes 0, 0 (in place in the session's buffer, as today).src/program/generate.cpp: the per-stage buffer table and device word,pl_ring_sel(an undo's replay in place in the restored buffer and clear the snapshot records; a speculative commit from the current buffer into one that is neither current nor recorded, recording the old one as the window's snapshot; anything else in place),pl_ring_restore, andpl_ring_settleat the start of a pipelined request and afterpl_drain: if a stage's state is not in its session's buffer, that stage's layer range is copied back once (29.4 MB here) and the word reset, so the serial paths, checkpoints, parking andSTRATA_STATE_HASHnever see the ring.snap_takeand the undo's restore copy nothing when the ring is on. Three buffers are enough for the 2-deep loop: two snapshot records at most plus the current buffer.STRATA_PIPELINE_SNAP_OVERLAP=0), withSTRATA_QFUSE,STRATA_FUSE_CONVAB,STRATA_COMMIT_BATCH=0,STRATA_GDN_SPLIT=1,STRATA_GDN_COMMIT_SPLIT=0orSTRATA_DBG_GDN=1, on Metal, or when a verifier's graphs were captured before the ring was set.src/kernels/gdn_parity.cppsection 7: the ring's kernels against today's.Validation
Environment note: measured only on PH402 SKU 200 boards - four GP100 dies of 48 SMs and 32 GB HBM2 each (128 GB in all), application clocks 715 / 1050 MHz - with every expert resident in VRAM (no CPU share, no PCIe share, no adaptive tier). The single-die check uses one of those dies. Nothing in the change is specific to that card, but no other card was run.
Rig: Windows 11, driver 581.80 (TCC), CUDA 12.9, VS 2022; build
-DSTRATA_EXPERIMENTAL_SM60=ON -DCMAKE_CUDA_ARCHITECTURES=60, ggml at the pinned 3cf03257f. Model: Swift 1.5 IQ3_S GGUF.Passed:
STRATA_Q8_SM60=1 gdn_parity --selftest:gdn_commit_layers_ring == in place: bitwise (8 cases, 5 layers),ring window kernels == today's: bitwise (16 cases),gdn: 0 failures,gdn_parity OK(the existing sections, includinggdn_commit_layers == per-layer commit, pass as on main). Section 7 checks (a)gdn_commit_layers_ringfrom buffer 0 into buffer 1, and in place in buffer 2, againstgdn_commit_layersin place, n_keep 1..8, five layers starting one layer into the buffers: memcmp equal, the source buffer and the layer before untouched, the device word set to the buffer written; (b) the window's conv and recurrence through the word againstgdn_conv_l2_multi/gdn_step_norm_multi's verify path, T 1..8, outputs from row 0 and from T/2: memcmp equal.STRATA_EXPERIMENTAL_SM60(strata,gdn_parity), clean.--layer-split 12,24,36,--pipeline-windows 2,--max-context 262144, environmentSTRATA_Q8_SM60=1 STRATA_MTP_Q8_SM60=1 STRATA_STATE_HASH=1and nothing else, one fresh engine per run, greedy, six requests (a three-turn conversation of about 3.5K tokens, three decode prompts of 600 tokens out). Every run with the switch loggedthe GDN snapshot ring.STRATA_STATE_HASHitem.STRATA_PIPELINE_DEBUG=1 STRATA_PIPELINE_FORCE_MISS=3, decode 52.3 against 52.7 tok/s): 6 of 6 replies and 6 of 6 sessions equal between off and on, so every undo restored the same state.STRATA_MMQ_WS=1, opt-in) #3752, a variant of [Engine] GP100 (sm_60): the decode down projection staged in shared memory, bitwise expert mode 0 (opt-in, STRATA_DOWN_WS=1) #3867, a--pipeline-windows 3loop and others not posted), Swift 1.5 IQ3_S, 1M context,--layer-split 12,24,36,--pipeline-windows 3, MTP, one fresh engine per arm, 13 requests per arm (five short prompts, 8K / 26K / 88K-token contexts with 300-400 tokens of decode, 1K / 2K / 6.9K / 30K reads, a 151K read with 800 tokens of decode), two runs:STRATA_PIPELINE_FORCE_MISS=3with two more local debug switches of my stack (not in this PR) that force wrong guesses on the later stages too and hash every stage's GDN, KV, index and PLE state after every request, plusSTRATA_STATE_HASH=1, eight decode requests. 3-deep loop: 8/8 equal to the copying loop in one run (1,284 undone windows), 7/8 in the other, where the one difference is the end position of a request whose last window was cut bymax_tokens(the count committed there depends on timing and is not comparable; the same request was equal in the other run). Today's 2-deep loop (main's), forced throughSTRATA_PIPELINE_SWITCHwithpw=2: 8/8 equal (1,231 undone windows).cuobjdumpofverify_kernels.cu(sm_60) showed the existing kernels' SASS unchanged with the switch's code compiled in; the four instances ofgdn_step_norm_multi_kernel, whose template gained the parameter, have the same instruction bodies. Not repeated on this branch.Failed: none observed.
Not run:
Limits