Skip to main content

softgpu_functional/
exec.rs

1//! Deterministic SoftGPU Functional IR interpreter.
2
3use crate::error::{FunctionalError, Result};
4use crate::ir::{Op, Program, TypeId};
5use crate::memory::{kernarg_load, GlobalArena};
6use serde::Serialize;
7use std::collections::BTreeMap;
8
9/// Marker required on all functional-mode outputs.
10pub const FUNCTIONAL_MODE_MARKER: &str = "softgpu_functional_cpu_not_gfx1201_isa";
11
12/// SoftGPU software launch limits (not hardware).
13pub const MAX_FLAT_WORKITEMS: u64 = 1_048_576;
14pub const MAX_WORKGROUP_FLAT: u32 = 1_024;
15pub const DEFAULT_STEP_BUDGET: u64 = 50_000_000;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct LaunchConfig {
19    pub grid: [u32; 3],
20    pub workgroup: [u32; 3],
21}
22
23impl LaunchConfig {
24    pub fn validate(&self) -> Result<()> {
25        for d in 0..3 {
26            if self.workgroup[d] == 0 {
27                return Err(FunctionalError::Validation {
28                    detail: format!("workgroup[{d}] must be > 0"),
29                });
30            }
31            if self.grid[d] == 0 {
32                return Err(FunctionalError::Validation {
33                    detail: format!("grid[{d}] must be > 0"),
34                });
35            }
36            if self.grid[d] % self.workgroup[d] != 0 {
37                return Err(FunctionalError::Validation {
38                    detail: format!(
39                        "grid[{d}]={} not divisible by workgroup[{d}]={}",
40                        self.grid[d], self.workgroup[d]
41                    ),
42                });
43            }
44        }
45        let wg_flat = u64::from(self.workgroup[0])
46            * u64::from(self.workgroup[1])
47            * u64::from(self.workgroup[2]);
48        if wg_flat > u64::from(MAX_WORKGROUP_FLAT) {
49            return Err(FunctionalError::Validation {
50                detail: format!("workgroup flat {wg_flat} > {MAX_WORKGROUP_FLAT}"),
51            });
52        }
53        let flat = u64::from(self.grid[0]) * u64::from(self.grid[1]) * u64::from(self.grid[2]);
54        if flat > MAX_FLAT_WORKITEMS {
55            return Err(FunctionalError::Validation {
56                detail: format!("flat workitems {flat} > {MAX_FLAT_WORKITEMS}"),
57            });
58        }
59        Ok(())
60    }
61
62    pub fn num_workgroups(&self) -> [u32; 3] {
63        [
64            self.grid[0] / self.workgroup[0],
65            self.grid[1] / self.workgroup[1],
66            self.grid[2] / self.workgroup[2],
67        ]
68    }
69}
70
71#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
72pub struct RunReport {
73    pub fidelity: &'static str,
74    pub mode: &'static str,
75    pub note: &'static str,
76    pub kernel: String,
77    pub workitems_executed: u64,
78    pub steps: u64,
79    pub grid: [u32; 3],
80    pub workgroup: [u32; 3],
81}
82
83/// Execute `program` for every workitem under a deterministic schedule:
84/// workgroups in lexicographic order, then local ids lexicographically.
85pub fn run(
86    program: &Program,
87    launch: LaunchConfig,
88    arena: &mut GlobalArena,
89    kernarg: &[u8],
90) -> Result<RunReport> {
91    run_with_budget(program, launch, arena, kernarg, DEFAULT_STEP_BUDGET)
92}
93
94pub fn run_with_budget(
95    program: &Program,
96    launch: LaunchConfig,
97    arena: &mut GlobalArena,
98    kernarg: &[u8],
99    step_budget: u64,
100) -> Result<RunReport> {
101    program.validate()?;
102    launch.validate()?;
103
104    let nwg = launch.num_workgroups();
105    let mut steps = 0u64;
106    let mut workitems = 0u64;
107
108    for gz in 0..nwg[2] {
109        for gy in 0..nwg[1] {
110            for gx in 0..nwg[0] {
111                for lz in 0..launch.workgroup[2] {
112                    for ly in 0..launch.workgroup[1] {
113                        for lx in 0..launch.workgroup[0] {
114                            let global = [
115                                gx * launch.workgroup[0] + lx,
116                                gy * launch.workgroup[1] + ly,
117                                gz * launch.workgroup[2] + lz,
118                            ];
119                            let local = [lx, ly, lz];
120                            let wg = [gx, gy, gz];
121                            let used = run_workitem(
122                                program,
123                                arena,
124                                kernarg,
125                                global,
126                                local,
127                                wg,
128                                step_budget.saturating_sub(steps),
129                            )?;
130                            steps = steps.saturating_add(used);
131                            if steps > step_budget {
132                                return Err(FunctionalError::StepBudgetExceeded { steps });
133                            }
134                            workitems += 1;
135                        }
136                    }
137                }
138            }
139        }
140    }
141
142    Ok(RunReport {
143        fidelity: "functional",
144        mode: FUNCTIONAL_MODE_MARKER,
145        note: "not_gfx1201_isa_emulation",
146        kernel: program.name.clone(),
147        workitems_executed: workitems,
148        steps,
149        grid: launch.grid,
150        workgroup: launch.workgroup,
151    })
152}
153
154fn run_workitem(
155    program: &Program,
156    arena: &mut GlobalArena,
157    kernarg: &[u8],
158    global: [u32; 3],
159    local: [u32; 3],
160    workgroup: [u32; 3],
161    budget: u64,
162) -> Result<u64> {
163    let mut regs: BTreeMap<String, i64> = BTreeMap::new();
164    let mut steps = 0u64;
165    for op in &program.body {
166        steps += 1;
167        if steps > budget {
168            return Err(FunctionalError::StepBudgetExceeded { steps });
169        }
170        match op {
171            Op::Const { dst, ty, value } => {
172                regs.insert(dst.clone(), narrow(*value, *ty)?);
173            }
174            Op::GlobalId { dst, dim } => {
175                regs.insert(dst.clone(), i64::from(global[*dim as usize]));
176            }
177            Op::LocalId { dst, dim } => {
178                regs.insert(dst.clone(), i64::from(local[*dim as usize]));
179            }
180            Op::WorkgroupId { dst, dim } => {
181                regs.insert(dst.clone(), i64::from(workgroup[*dim as usize]));
182            }
183            Op::Add { dst, lhs, rhs, ty } => {
184                let a = get_reg(&regs, lhs)?;
185                let b = get_reg(&regs, rhs)?;
186                regs.insert(dst.clone(), narrow(a.wrapping_add(b), *ty)?);
187            }
188            Op::Sub { dst, lhs, rhs, ty } => {
189                let a = get_reg(&regs, lhs)?;
190                let b = get_reg(&regs, rhs)?;
191                regs.insert(dst.clone(), narrow(a.wrapping_sub(b), *ty)?);
192            }
193            Op::Mul { dst, lhs, rhs, ty } => {
194                let a = get_reg(&regs, lhs)?;
195                let b = get_reg(&regs, rhs)?;
196                regs.insert(dst.clone(), narrow(a.wrapping_mul(b), *ty)?);
197            }
198            Op::KernargLoad { dst, offset, ty } => {
199                regs.insert(dst.clone(), kernarg_load(kernarg, *offset, *ty)?);
200            }
201            Op::LoadGlobal { dst, addr, ty } => {
202                let a = get_reg(&regs, addr)? as u64;
203                regs.insert(dst.clone(), arena.load(a, *ty)?);
204            }
205            Op::StoreGlobal { addr, src, ty } => {
206                let a = get_reg(&regs, addr)? as u64;
207                let v = get_reg(&regs, src)?;
208                arena.store(a, *ty, v)?;
209            }
210            Op::Ret => return Ok(steps),
211        }
212    }
213    Err(FunctionalError::Internal(
214        "fell off end of body without ret".into(),
215    ))
216}
217
218fn get_reg(regs: &BTreeMap<String, i64>, name: &str) -> Result<i64> {
219    regs.get(name)
220        .copied()
221        .ok_or_else(|| FunctionalError::UndefinedReg {
222            name: name.to_string(),
223        })
224}
225
226fn narrow(v: i64, ty: TypeId) -> Result<i64> {
227    Ok(match ty {
228        TypeId::I32 => v as i32 as i64,
229        TypeId::U32 => (v as u32) as i64,
230        TypeId::U64 => v,
231    })
232}