vyre_driver/program_walks/
dispatch_params.rs1use vyre_foundation::ir::Program;
4
5use crate::binding::{Binding, BindingRole};
6
7#[must_use]
9pub fn dispatch_element_count(bindings: &[Binding]) -> u32 {
10 dispatch_element_count_inner(bindings, false)
11}
12
13#[must_use]
19pub fn dispatch_element_count_for_program(program: &Program, bindings: &[Binding]) -> u32 {
20 let capabilities = vyre_foundation::program_caps::scan(program);
21 dispatch_element_count_inner(
22 bindings,
23 program_contains_atomic(program) || capabilities.subgroup_ops,
24 )
25}
26
27fn dispatch_element_count_inner(bindings: &[Binding], force_full_span: bool) -> u32 {
28 let mut any_shared = false;
34 let mut max_non_shared: u32 = 0;
35 let mut max_output: u32 = 0;
36 for binding in bindings {
37 if binding.role == BindingRole::Shared {
38 any_shared = true;
39 continue;
40 }
41 if binding.element_count > max_non_shared {
42 max_non_shared = binding.element_count;
43 }
44 if matches!(binding.role, BindingRole::Output | BindingRole::InputOutput)
45 && binding.element_count > max_output
46 {
47 max_output = binding.element_count;
48 }
49 }
50 if any_shared || force_full_span {
51 return max_non_shared.max(1);
52 }
53 if max_output > 0 {
54 return max_output;
55 }
56 max_non_shared.max(1)
57}
58
59fn program_contains_atomic(program: &Program) -> bool {
60 program.stats().atomic_op_count > 0
65}
66
67pub fn try_dispatch_param_words(
70 bindings: &[Binding],
71 element_count: u32,
72) -> Result<Vec<u32>, String> {
73 let mut words = Vec::new();
74 try_dispatch_param_words_into(bindings, element_count, &mut words)?;
75 Ok(words)
76}
77
78pub fn dispatch_param_words_into(
87 bindings: &[Binding],
88 element_count: u32,
89 words: &mut Vec<u32>,
90) -> Result<(), String> {
91 try_dispatch_param_words_into(bindings, element_count, words)
92}
93
94pub fn try_dispatch_param_words_into(
97 bindings: &[Binding],
98 element_count: u32,
99 words: &mut Vec<u32>,
100) -> Result<(), String> {
101 let word_len = dispatch_param_word_len_for_bindings(bindings)?;
102 reserve_dispatch_param_words(words, word_len)?;
103 words.clear();
104 words.resize(word_len, 0);
105 words[0] = element_count;
106 for binding in bindings {
107 if binding.role == BindingRole::Shared {
108 continue;
109 }
110 let slot = dispatch_param_word_slot(binding)?;
111 words[slot] = if binding.element_count == 0 {
112 element_count
113 } else {
114 binding.element_count
115 };
116 }
117 Ok(())
118}
119
120fn dispatch_param_word_len_for_bindings(bindings: &[Binding]) -> Result<usize, String> {
121 let mut word_len = dispatch_param_word_len_checked(bindings.len())?;
122 for binding in bindings {
123 if binding.role == BindingRole::Shared {
124 continue;
125 }
126 let required = dispatch_param_word_slot(binding)?
127 .checked_add(1)
128 .ok_or_else(|| {
129 format!(
130 "dispatch binding slot {} overflows ABI parameter word count. Fix: split the Program before launch-parameter planning.",
131 binding.binding
132 )
133 })?;
134 if required > word_len {
135 word_len = required;
136 }
137 }
138 Ok(word_len)
139}
140
141fn dispatch_param_word_slot(binding: &Binding) -> Result<usize, String> {
142 let slot = usize::try_from(binding.binding).map_err(|error| {
143 format!(
144 "dispatch binding slot {} does not fit host usize ({error}). Fix: split the Program before launch-parameter planning.",
145 binding.binding
146 )
147 })?;
148 slot.checked_add(1).ok_or_else(|| {
149 format!(
150 "dispatch binding slot {} overflows ABI parameter slot. Fix: split the Program before launch-parameter planning.",
151 binding.binding
152 )
153 })
154}
155
156fn dispatch_param_word_len_checked(binding_count: usize) -> Result<usize, String> {
157 binding_count.checked_add(1).ok_or_else(|| {
158 format!(
159 "dispatch binding count {binding_count} overflows ABI parameter word count. Fix: split the Program before launch-parameter planning."
160 )
161 })
162}
163
164fn reserve_dispatch_param_words(words: &mut Vec<u32>, word_len: usize) -> Result<(), String> {
165 crate::allocation::try_reserve_vec_to_capacity(words, word_len).map_err(|error| {
166 format!(
167 "dispatch parameter staging could not reserve {word_len} u32 word(s): {error}. Fix: split the Program before launch-parameter planning."
168 )
169 })
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::binding::BindingRole;
176 use std::sync::Arc;
177
178 fn binding(buffer_index: usize, element_count: u32) -> Binding {
179 Binding {
180 name: Arc::from("test"),
181 binding: u32::try_from(buffer_index).expect("Fix: test binding index fits u32"),
182 buffer_index,
183 role: BindingRole::Input,
184 element_size: 4,
185 preferred_alignment: 4,
186 element_count,
187 static_byte_len: None,
188 input_index: Some(0),
189 output_index: None,
190 }
191 }
192
193 #[test]
197 fn subgroup_collective_dispatch_uses_full_input_span() {
198 let mut output = binding(0, 16);
199 output.role = BindingRole::Output;
200 output.output_index = Some(0);
201 output.input_index = None;
202 let input = binding(1, 512);
203 let program = vyre_foundation::ir::Program::wrapped(
204 Vec::new(),
205 [256, 1, 1],
206 vec![vyre_foundation::ir::Node::let_bind(
207 "mask",
208 vyre_foundation::ir::Expr::subgroup_ballot(vyre_foundation::ir::Expr::bool(true)),
209 )],
210 );
211
212 assert_eq!(
213 dispatch_element_count_for_program(&program, &[output, input]),
214 512
215 );
216 }
217
218 #[test]
219 fn dispatch_params_support_sparse_binding_indices_without_repeated_growth() {
220 let bindings = [binding(4, 9), binding(1, 0)];
221 let words = try_dispatch_param_words(&bindings, 7)
222 .expect("Fix: sparse binding parameter words should stage");
223
224 assert_eq!(words, vec![7, 0, 7, 0, 0, 9]);
225 }
226
227 #[test]
228 fn dispatch_params_are_indexed_by_binding_slot_not_program_buffer_index() {
229 let mut sparse = binding(0, 4);
230 sparse.binding = 9;
231 let mut dynamic = binding(7, 0);
232 dynamic.binding = 2;
233 let words = try_dispatch_param_words(&[sparse, dynamic], 11)
234 .expect("Fix: sparse binding-slot parameter words should stage");
235
236 assert_eq!(words.len(), 11);
237 assert_eq!(words[0], 11);
238 assert_eq!(words[3], 11);
239 assert_eq!(words[10], 4);
240 assert_eq!(
241 words[8], 0,
242 "Fix: CUDA/PTX parameter words are indexed by binding slot, not buffer_index."
243 );
244 }
245
246 #[test]
247 fn generated_dispatch_params_cover_sparse_binding_slot_matrix() {
248 let mut checked = 0usize;
249 for seed in 0..4096u32 {
250 let binding_count = (seed as usize % 8) + 1;
251 let mut bindings = Vec::with_capacity(binding_count);
252 for index in 0..binding_count {
253 let mut item = binding(
254 index,
255 if index % 3 == 0 {
256 0
257 } else {
258 seed + index as u32
259 },
260 );
261 item.binding = ((seed.wrapping_mul(17) + (index as u32 * 97)) % 1024) + 1;
262 item.buffer_index = binding_count - 1 - index;
263 bindings.push(item);
264 }
265 let element_count = seed.wrapping_mul(31) | 1;
266 let words = try_dispatch_param_words(&bindings, element_count)
267 .expect("Fix: generated sparse binding-slot param words should stage.");
268 assert_eq!(words[0], element_count, "seed {seed}");
269 for item in &bindings {
270 let slot = usize::try_from(item.binding).expect("Fix: test binding fits usize") + 1;
271 let expected = if item.element_count == 0 {
272 element_count
273 } else {
274 item.element_count
275 };
276 assert_eq!(
277 words[slot], expected,
278 "Fix: generated dispatch-param seed {seed} binding slot {} must map to words[slot+1], regardless of buffer_index {}.",
279 item.binding, item.buffer_index
280 );
281 checked += 1;
282 }
283 }
284 assert!(
285 checked >= 18_000,
286 "Fix: generated dispatch-param ABI coverage should exercise thousands of sparse binding layouts, got {checked}."
287 );
288 }
289
290 #[test]
298 fn dispatch_param_words_into_propagates_error_instead_of_silently_clearing() {
299 let bindings = [binding(0, 7), binding(2, 3)];
301 let mut words = Vec::new();
302 dispatch_param_words_into(&bindings, 7, &mut words)
303 .expect("Fix: dispatch_param_words_into must succeed for valid bindings");
304 assert_eq!(
305 words,
306 vec![7, 7, 0, 3],
307 "Fix: dispatch_param_words_into must produce [element_count, b0, b1_zero, b2] for valid bindings"
308 );
309
310 #[cfg(target_pointer_width = "32")]
321 {
322 let mut overflow_binding = binding(0, 5);
323 overflow_binding.binding = u32::MAX;
324 let mut words2 = vec![0xdead_beef_u32; 4];
325 let err = dispatch_param_words_into(&[overflow_binding], 5, &mut words2);
326 assert!(
327 err.is_err(),
328 "Fix: dispatch_param_words_into must return Err on ABI slot overflow, not silently clear the buffer"
329 );
330 let msg = err.unwrap_err();
331 assert!(
332 msg.contains("Fix:"),
333 "Fix: dispatch_param_words_into error message must include a Fix: hint, got: {msg}"
334 );
335 }
336 }
337}