metrale_circuit/hardware/
tc_policy.rs1use std::collections::BTreeSet;
21
22use crate::format::Format;
23use crate::fuser::FusionPlan;
24use crate::ir::{Circuit, LinearRole, OpKind};
25use crate::rules::{KernelId, Mode};
26use crate::venn::compute::{ComputeUnit, group_unit};
27use crate::venn::families::Families;
28
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
31pub struct OpMatch {
32 pub base: String,
34 pub role: Option<LinearRole>,
36}
37
38impl OpMatch {
39 fn parse(s: &str) -> Result<Self, String> {
40 let (base, role) = match s.split_once(':') {
41 Some(("linear", r)) => (
42 "linear",
43 Some(LinearRole::parse(r).ok_or_else(|| format!("unknown linear role `{r}`"))?),
44 ),
45 Some(_) => return Err(format!("op `{s}`: only `linear` takes a role")),
46 None => (s, None),
47 };
48 if base != "linear" {
49 OpKind::parse(base, None, None).map_err(|e| e.to_string())?;
50 }
51 Ok(OpMatch {
52 base: base.to_string(),
53 role,
54 })
55 }
56
57 fn matches(&self, op: &OpKind) -> bool {
58 op.base_name() == self.base
59 && match (op, self.role) {
60 (OpKind::Linear(r), Some(want)) => *r == want,
61 _ => true,
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum Weights {
69 Any,
71 Only(BTreeSet<Format>),
73}
74
75impl Weights {
76 fn covers(&self, w: Option<Format>) -> bool {
77 match self {
78 Weights::Any => true,
79 Weights::Only(set) => w.is_some_and(|w| set.contains(&w)),
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Require {
87 pub ops: Vec<OpMatch>,
89 pub modes: BTreeSet<Mode>,
91 pub min_rows: u64,
93 pub weights: Weights,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
99pub enum ExemptKind {
100 Measured,
102 Shape,
104 Backlog,
106}
107
108impl ExemptKind {
109 pub fn name(self) -> &'static str {
111 match self {
112 ExemptKind::Measured => "measured",
113 ExemptKind::Shape => "shape",
114 ExemptKind::Backlog => "backlog",
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Exempt {
122 pub ops: Vec<OpMatch>,
124 pub modes: BTreeSet<Mode>,
126 pub rows: (u64, u64),
128 pub kernels: BTreeSet<KernelId>,
130 pub kind: ExemptKind,
132 pub reason: String,
134 pub evidence: Option<String>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct TcPolicy {
141 pub require: Vec<Require>,
143 pub exempt: Vec<Exempt>,
145}
146
147#[path = "tc_policy_parse.rs"]
148mod parse;
149pub use parse::parse_policy;
150#[path = "tc_policy_lint.rs"]
151mod lint;
152pub use lint::lint_rules;
153
154#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
156pub struct Finding {
157 pub site: String,
159 pub op: String,
161 pub weight: Option<String>,
163 pub mode: Mode,
165 pub rows: u64,
167 pub kernels: Vec<KernelId>,
169 pub unit: String,
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct TcAudit {
176 pub covered: usize,
178 pub on_tensor_cores: usize,
180 pub exempted: Vec<(Finding, usize)>,
182 pub violations: Vec<Finding>,
184 pub gaps: Vec<Finding>,
187}
188
189pub fn unit_of_group(
192 families: &Families,
193 kernels: &[KernelId],
194) -> Result<Option<ComputeUnit>, String> {
195 let mut units = Vec::with_capacity(kernels.len());
196 for k in kernels {
197 units.push(families.compute_of(k).ok_or_else(|| {
198 format!("kernel `{k}` is in no kernel family, so its compute unit is undeclared")
199 })?);
200 }
201 Ok(group_unit(units))
202}
203
204pub fn unit_tag(families: &Families, kernels: &[KernelId]) -> Option<String> {
207 match unit_of_group(families, kernels) {
208 Ok(u) => u.map(|u| u.tag()),
209 Err(_) => Some("undeclared".to_string()),
210 }
211}
212
213pub fn audit(
217 c: &Circuit,
218 plan: &FusionPlan,
219 families: &Families,
220 policy: &TcPolicy,
221 skip_emitter: &str,
222) -> Result<TcAudit, String> {
223 let (mode, rows) = (plan.mode, plan.rows);
224 let mut out = TcAudit::default();
225 let mut seen = BTreeSet::new();
226 for g in &plan.groups {
227 let placeholder = g.emitter == skip_emitter;
228 let mut unit: Option<Option<ComputeUnit>> = None;
229 for &n in &g.nodes {
230 let node = &c.nodes[n];
231 let covered = policy.require.iter().any(|r| {
232 r.modes.contains(&mode)
233 && rows >= r.min_rows
234 && r.weights.covers(node.weight)
235 && r.ops.iter().any(|o| o.matches(&node.op))
236 });
237 if !covered {
238 continue;
239 }
240 if placeholder {
241 let f = Finding {
242 site: format!("{}.{}", node.block, node.local),
243 op: node.op.name(),
244 weight: node.weight.map(|w| w.name()),
245 mode,
246 rows,
247 kernels: Vec::new(),
248 unit: "no planned kernel".to_string(),
249 };
250 if seen.insert(f.clone()) {
251 out.gaps.push(f);
252 }
253 continue;
254 }
255 out.covered += 1;
256 let u = match &unit {
257 Some(u) => u.clone(),
258 None => {
259 let u = unit_of_group(families, &g.kernels)?;
260 unit = Some(u.clone());
261 u
262 }
263 };
264 if u.as_ref().is_some_and(ComputeUnit::is_tensor_core) {
265 out.on_tensor_cores += 1;
266 continue;
267 }
268 let f = Finding {
269 site: format!("{}.{}", node.block, node.local),
270 op: node.op.name(),
271 weight: node.weight.map(|w| w.name()),
272 mode,
273 rows,
274 kernels: g.kernels.clone(),
275 unit: u.map_or_else(|| "host".to_string(), |u| u.name()),
276 };
277 if !seen.insert(f.clone()) {
278 continue;
279 }
280 let allowed = policy.exempt.iter().position(|e| {
281 e.modes.contains(&mode)
282 && (e.rows.0..=e.rows.1).contains(&rows)
283 && e.ops.iter().any(|o| o.matches(&node.op))
284 && !g.kernels.is_empty()
285 && g.kernels.iter().all(|k| e.kernels.contains(k))
286 });
287 match allowed {
288 Some(i) => out.exempted.push((f, i)),
289 None => out.violations.push(f),
290 }
291 }
292 }
293 Ok(out)
294}
295
296pub fn describe(violations: &[Finding]) -> String {
298 violations
299 .iter()
300 .map(|f| {
301 let k: Vec<String> = f.kernels.iter().map(|k| k.to_string()).collect();
302 format!(
303 "{} {} ({}) at {} n={} runs on {} [{}]",
304 f.site,
305 f.op,
306 f.weight.as_deref().unwrap_or("no weight"),
307 f.mode.name(),
308 f.rows,
309 f.unit,
310 if k.is_empty() {
311 "no kernel".to_string()
312 } else {
313 k.join(" + ")
314 }
315 )
316 })
317 .collect::<Vec<_>>()
318 .join("\n ")
319}
320
321pub fn enforce(
324 r: &super::plan::Resolved,
325 c: &Circuit,
326 planned: &super::plan::Planned,
327) -> Result<Option<TcAudit>, super::HwError> {
328 let Some(policy) = &r.chain[0].tensor_core else {
329 return Ok(None);
330 };
331 let mut total = TcAudit::default();
332 let arms = std::iter::once(planned).chain(planned.routes.iter().map(|rp| &rp.planned));
333 for p in arms {
334 let a = audit(c, &p.plan, &r.families, policy, super::plan::NOVEL_EMITTER)
335 .map_err(super::HwError::Plan)?;
336 total.covered += a.covered;
337 total.on_tensor_cores += a.on_tensor_cores;
338 for e in a.exempted {
339 if !total.exempted.contains(&e) {
340 total.exempted.push(e);
341 }
342 }
343 for v in a.violations {
344 if !total.violations.contains(&v) {
345 total.violations.push(v);
346 }
347 }
348 for g in a.gaps {
349 if !total.gaps.contains(&g) {
350 total.gaps.push(g);
351 }
352 }
353 }
354 if !total.violations.is_empty() {
355 return Err(super::HwError::TensorCore {
356 class: r.chain[0].name.clone(),
357 violations: describe(&total.violations),
358 });
359 }
360 Ok(Some(total))
361}
362
363#[cfg(test)]
364#[path = "tc_policy_tests.rs"]
365mod tc_policy_tests;