Skip to main content

vyre_libs/decode/
streaming.rs

1//! Streaming decode → scan adapter (G5).
2//!
3//! # What this does
4//!
5//! `decode::base64 / hex / inflate / lz4` each used to produce a
6//! storage-buffer output that `matching::dfa / nfa` then re-read
7//! from DRAM. Two kernels, two DMA round-trips, one pipeline
8//! barrier  -  cheap on a 1 MiB corpus, ruinous on a 100 GiB corpus
9//! scan where the decoded-bytes footprint dominates the DRAM budget.
10//!
11//! The fused path hands bytes from decoder → scanner through
12//! **workgroup-shared memory** on the same dispatch. No DRAM
13//! round-trip, no pipeline barrier  -  the scanner's loads hit L1 on
14//! the SM that decoded the bytes.
15//!
16//! # Contract
17//!
18//! The caller supplies a `decoder` Program that *writes* a named
19//! handoff buffer and a `scanner` Program that *reads* the same
20//! named buffer. `fuse_decode_scan` merges them via the existing
21//! [`vyre_foundation::execution_plan::fusion::fuse_programs`] kernel
22//! fuser, then rewrites the handoff buffer's declaration so it lives
23//! in workgroup memory instead of storage.
24//!
25//! # Why this module lives in vyre-libs
26//!
27//! The fusion transformation itself is a foundation-layer pass
28//! (`optimizer::passes::decode_scan_fuse`). This module is the
29//! library-level API that consumers reach for directly. Both
30//! paths land on the same fused Program  -  the foundation pass is
31//! the canonical transformation, this is a thin convenience layer.
32
33use vyre_foundation::execution_plan::fusion::fuse_programs;
34use vyre_foundation::ir::{BufferDecl, DataType, Program};
35
36/// Error states surfaced by [`fuse_decode_scan`].
37#[derive(Debug, thiserror::Error)]
38pub enum DecodeScanFuseError {
39    /// The caller supplied `&decoder` and `&scanner` but neither
40    /// declares the named handoff buffer. Fusing would produce a
41    /// Program with no shared byte-flow path  -  the caller's intent
42    /// cannot be honoured.
43    #[error(
44        "Fix: handoff buffer {handoff:?} does not appear in the decoder or scanner Program's \
45         buffer list. Add a `BufferDecl::storage({handoff:?}, ..., DataType::U32)` to both \
46         Programs before calling `fuse_decode_scan`."
47    )]
48    HandoffBufferMissing {
49        /// Name of the missing decoder/scanner handoff buffer.
50        handoff: String,
51    },
52    /// The caller passed `handoff_byte_count = 0`. Workgroup
53    /// allocations must be strictly positive or the fused Program
54    /// declares a zero-sized shared buffer that every driver
55    /// rejects. Returning an error instead of asserting (per
56    /// PHASE2_DECODE HIGH / LAW 5) keeps adversarial callers from
57    /// crashing the driver process.
58    #[error(
59        "Fix: fuse_decode_scan(handoff_byte_count = 0) is rejected on buffer {handoff:?}. \
60         Pass the decoder's peak output-bytes-per-workgroup."
61    )]
62    ZeroHandoff {
63        /// Name of the handoff buffer whose capacity was zero.
64        handoff: String,
65    },
66    /// `fuse_programs` rejected the pair (self-aliasing or
67    /// workgroup-size mismatch). The inner error is the original.
68    #[error(
69        "Fix: kernel-level fusion failed  -  run the autotune pass to normalise workgroup \
70         sizes and rename any self-aliasing buffers before calling `fuse_decode_scan`. \
71         Inner: {0}"
72    )]
73    Fusion(#[from] vyre_foundation::execution_plan::fusion::FusionError),
74}
75
76/// Fuse a decoder Program with a scanner Program into a single
77/// dispatch. The decoder writes `handoff_buf`; the scanner reads
78/// it. In the fused Program the handoff buffer is promoted to
79/// workgroup memory so its bytes never touch DRAM.
80///
81/// `handoff_byte_count` is the capacity the fused Program reserves
82/// for the workgroup handoff  -  typically the decoder's max output
83/// bytes per workgroup. Must be strictly positive.
84pub fn fuse_decode_scan(
85    decoder: Program,
86    scanner: Program,
87    handoff_buf: &str,
88    handoff_byte_count: u32,
89) -> Result<Program, DecodeScanFuseError> {
90    if handoff_byte_count == 0 {
91        return Err(DecodeScanFuseError::ZeroHandoff {
92            handoff: handoff_buf.to_string(),
93        });
94    }
95    let decoder_has = decoder.buffers.iter().any(|b| b.name() == handoff_buf);
96    let scanner_has = scanner.buffers.iter().any(|b| b.name() == handoff_buf);
97    if !decoder_has && !scanner_has {
98        return Err(DecodeScanFuseError::HandoffBufferMissing {
99            handoff: handoff_buf.to_string(),
100        });
101    }
102
103    let fused = fuse_programs(&[decoder, scanner])?;
104    Ok(promote_to_workgroup(fused, handoff_buf, handoff_byte_count))
105}
106
107fn promote_to_workgroup(program: Program, handoff_buf: &str, count: u32) -> Program {
108    // `BufferDecl::workgroup` already sets `access: Workgroup` and
109    // `kind: Shared`  -  no extra `.with_kind()` required.
110    let mut new_buffers: Vec<BufferDecl> = program
111        .buffers
112        .iter()
113        .filter(|b| b.name() != handoff_buf)
114        .cloned()
115        .collect();
116    new_buffers.push(BufferDecl::workgroup(handoff_buf, count, DataType::U32));
117    // Only the buffer table changes here, so replace only the buffer table.
118    // `Program::wrapped` would build a fresh program, resetting `entry_op_id`
119    // and `non_composable_with_self`, and would deep-clone the entry as well.
120    program.with_rewritten_buffers(new_buffers)
121}
122
123/// How many bytes of DRAM traffic one dispatch saves by fusing a
124/// decoder+scanner pair with an N-byte handoff. Used by the G12
125/// benchmark harness to verify the fusion is paying off.
126#[must_use]
127pub fn dram_bytes_saved(handoff_byte_count: u32, invocations: u32) -> u64 {
128    // Decoder would have written `handoff_byte_count` bytes to DRAM
129    // per invocation, scanner would have read the same. 2×.
130    2_u64 * u64::from(handoff_byte_count) * u64::from(invocations)
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use vyre_foundation::ir::{BufferAccess, Expr, MemoryKind, Node};
137
138    fn decoder_with_handoff(handoff: &str) -> Program {
139        Program::wrapped(
140            vec![
141                BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
142                    .with_count(64),
143                BufferDecl::storage(handoff, 1, BufferAccess::ReadWrite, DataType::U32)
144                    .with_count(64),
145            ],
146            [64, 1, 1],
147            vec![Node::store(
148                handoff,
149                Expr::InvocationId { axis: 0 },
150                Expr::u32(0xAA),
151            )],
152        )
153    }
154
155    fn scanner_with_handoff(handoff: &str) -> Program {
156        Program::wrapped(
157            vec![
158                BufferDecl::storage(handoff, 1, BufferAccess::ReadOnly, DataType::U32)
159                    .with_count(64),
160                BufferDecl::storage("matches", 2, BufferAccess::ReadWrite, DataType::U32)
161                    .with_count(64),
162            ],
163            [64, 1, 1],
164            vec![Node::let_bind(
165                "byte",
166                Expr::load(handoff, Expr::InvocationId { axis: 0 }),
167            )],
168        )
169    }
170
171    #[test]
172    fn missing_handoff_buffer_errors_with_actionable_fix() {
173        let decoder = Program::wrapped(
174            vec![
175                BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
176                    .with_count(1),
177            ],
178            [64, 1, 1],
179            vec![],
180        );
181        let scanner = Program::wrapped(
182            vec![
183                BufferDecl::storage("matches", 0, BufferAccess::ReadWrite, DataType::U32)
184                    .with_count(1),
185            ],
186            [64, 1, 1],
187            vec![],
188        );
189        let err = fuse_decode_scan(decoder, scanner, "decoded", 64).unwrap_err();
190        let msg = format!("{err}");
191        assert!(msg.contains("Fix:"));
192        assert!(msg.contains("decoded"));
193    }
194
195    #[test]
196    fn zero_handoff_byte_count_returns_structured_error() {
197        let decoder = decoder_with_handoff("decoded");
198        let scanner = scanner_with_handoff("decoded");
199        let err = fuse_decode_scan(decoder, scanner, "decoded", 0).unwrap_err();
200        assert!(matches!(err, DecodeScanFuseError::ZeroHandoff { .. }));
201        assert!(err.to_string().contains("Fix:"));
202    }
203
204    #[test]
205    fn fused_program_promotes_handoff_to_workgroup_memory() {
206        let decoder = decoder_with_handoff("decoded");
207        let scanner = scanner_with_handoff("decoded");
208        let fused = fuse_decode_scan(decoder, scanner, "decoded", 128).unwrap();
209        let handoff = fused.buffers.iter().find(|b| b.name() == "decoded").expect(
210            "Fix: handoff buffer survives fusion; restore this invariant before continuing.",
211        );
212        assert_eq!(handoff.access(), BufferAccess::Workgroup);
213        assert_eq!(handoff.kind(), MemoryKind::Shared);
214        assert_eq!(handoff.count(), 128);
215    }
216
217    #[test]
218    fn non_handoff_buffers_stay_as_declared() {
219        let decoder = decoder_with_handoff("decoded");
220        let scanner = scanner_with_handoff("decoded");
221        let fused = fuse_decode_scan(decoder, scanner, "decoded", 128).unwrap();
222        let input = fused.buffers.iter().find(|b| b.name() == "input").unwrap();
223        assert_eq!(input.access(), BufferAccess::ReadOnly);
224        let matches = fused
225            .buffers
226            .iter()
227            .find(|b| b.name() == "matches")
228            .unwrap();
229        assert_eq!(matches.access(), BufferAccess::ReadWrite);
230    }
231
232    #[test]
233    fn fused_body_contains_both_decoder_and_scanner_nodes() {
234        let decoder = decoder_with_handoff("decoded");
235        let scanner = scanner_with_handoff("decoded");
236        let fused = fuse_decode_scan(decoder, scanner, "decoded", 64).unwrap();
237        // `Program::wrapped` always normalizes a multi-node entry into a
238        // single root `Node::Region` that owns the per-arm body; check
239        // that body holds the two arms (plus any inserted barrier).
240        assert_eq!(
241            fused.entry.len(),
242            1,
243            "wrapped entry must be a single root Region"
244        );
245        let body = match &fused.entry[0] {
246            vyre_foundation::ir::Node::Region { body, .. } => body.as_ref(),
247            other => panic!("Fix: fused entry root must be a Region, got {other:?}"),
248        };
249        assert!(
250            body.len() >= 2,
251            "fused root region body should contain both arms, got {} nodes",
252            body.len()
253        );
254    }
255
256    #[test]
257    fn dram_bytes_saved_scales_with_invocations() {
258        assert_eq!(dram_bytes_saved(0, 1_000_000), 0);
259        assert_eq!(dram_bytes_saved(1024, 1000), 2 * 1024 * 1000);
260        assert_eq!(dram_bytes_saved(1, u32::MAX), 2 * u64::from(u32::MAX));
261    }
262
263    #[test]
264    fn handoff_present_in_only_scanner_still_fuses() {
265        let decoder = Program::wrapped(
266            vec![
267                BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
268                    .with_count(1),
269            ],
270            [64, 1, 1],
271            vec![],
272        );
273        let scanner = scanner_with_handoff("decoded");
274        let fused = fuse_decode_scan(decoder, scanner, "decoded", 64).unwrap();
275        let handoff = fused
276            .buffers
277            .iter()
278            .find(|b| b.name() == "decoded")
279            .unwrap();
280        assert_eq!(handoff.access(), BufferAccess::Workgroup);
281    }
282}