metrale_circuit/pipeline/
declare.rs1use std::collections::{BTreeMap, BTreeSet};
28
29use super::vocab::{parse_step, parse_value, step_name, steps_for_base};
30use super::{NodePipeline, Step, StepKind};
31use crate::format::Format;
32use crate::ir::{LinearRole, Node, OpKind};
33use crate::rules::KernelId;
34use crate::venn::families::{OpSpec, Values};
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
38pub struct RawPipeline {
39 pub inputs: Vec<String>,
41 pub steps: Vec<(StepKind, String)>,
43 pub outputs: Vec<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
49pub enum Entry {
50 Pipeline(RawPipeline),
52 Uninstantiated,
54}
55
56pub type ByOp = BTreeMap<String, Entry>;
58
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
62pub struct FamilyPipelines {
63 pub family: ByOp,
65 pub kernels: BTreeMap<KernelId, ByOp>,
67}
68
69pub fn parse_by_op(tables: &BTreeMap<String, toml::Value>) -> Result<ByOp, String> {
72 let mut out = ByOp::new();
73 for (key, v) in tables {
74 let entry = match v {
75 toml::Value::Table(t) => Entry::Pipeline(raw(key, t)?),
76 toml::Value::String(s) if s == "uninstantiated" => Entry::Uninstantiated,
77 _ => {
78 return Err(format!(
79 "pipeline `{key}` is neither a table nor \"uninstantiated\""
80 ));
81 }
82 };
83 out.insert(key.clone(), entry);
84 }
85 Ok(out)
86}
87
88struct Key<'a> {
91 op: &'a str,
92 base: &'a str,
93 role: Option<LinearRole>,
94}
95
96fn op_name_ok(name: &str) -> bool {
97 match name.split_once(':') {
98 Some(("linear", r)) => LinearRole::parse(r).is_some(),
99 Some(_) => false,
100 None => steps_for_base(name).is_some(),
101 }
102}
103
104fn base_of(key: &str) -> Result<Key<'_>, String> {
105 let (head, after, feeds) = {
106 let (head, feeds) = match key.split_once(" feeds ") {
107 Some((h, f)) => (h, Some(f)),
108 None => (key, None),
109 };
110 match head.split_once(" after ") {
111 Some((h, a)) => (h, Some(a), feeds),
112 None => (head, None, feeds),
113 }
114 };
115 if let Some(bad) = [after, feeds]
116 .into_iter()
117 .flatten()
118 .find(|n| !op_name_ok(n))
119 {
120 return Err(format!("pipeline `{key}`: `{bad}` is no op"));
121 }
122 let (base, role) = match head.split_once(':') {
123 None => (head, None),
124 Some(("linear", r)) => (
125 "linear",
126 Some(
127 LinearRole::parse(r)
128 .ok_or_else(|| format!("pipeline `{key}`: unknown linear role `{r}`"))?,
129 ),
130 ),
131 Some(("act_quant", f)) if crate::format::Format::parse(f).is_ok() => ("act_quant", None),
132 Some(_) => {
133 return Err(format!(
134 "pipeline `{key}`: only `linear` (a role) and `act_quant` (a format) qualify"
135 ));
136 }
137 };
138 Ok(Key {
139 op: head,
140 base,
141 role,
142 })
143}
144
145fn raw(key: &str, t: &toml::Table) -> Result<RawPipeline, String> {
146 let base = base_of(key)?.base;
147 let steps = steps_for_base(base).ok_or_else(|| format!("pipeline `{key}`: unknown op"))?;
148 let list = |field: &str| -> Result<Vec<String>, String> {
149 let arr = t
150 .get(field)
151 .and_then(|v| v.as_array())
152 .ok_or_else(|| format!("pipeline `{key}` states no `{field}` list"))?;
153 arr.iter()
154 .map(|v| {
155 v.as_str()
156 .map(str::to_string)
157 .ok_or_else(|| format!("pipeline `{key}` `{field}`: a non-string format"))
158 })
159 .collect()
160 };
161 for k in t.keys() {
162 let known = k == "in" || k == "out" || parse_step(k).is_some_and(|s| steps.contains(&s));
163 if !known {
164 return Err(format!(
165 "pipeline `{key}`: `{k}` is not a step of `{base}` ({})",
166 steps
167 .iter()
168 .map(|s| step_name(*s))
169 .collect::<Vec<_>>()
170 .join(", ")
171 ));
172 }
173 }
174 let mut out_steps = Vec::with_capacity(steps.len());
175 for &s in steps {
176 let v = t
177 .get(step_name(s))
178 .and_then(|v| v.as_str())
179 .ok_or_else(|| format!("pipeline `{key}` states no `{}`", step_name(s)))?;
180 out_steps.push((s, v.to_string()));
181 }
182 Ok(RawPipeline {
183 inputs: list("in")?,
184 steps: out_steps,
185 outputs: list("out")?,
186 })
187}
188
189pub fn validate(
192 ops: &[OpSpec],
193 kernels: &[KernelId],
194 decl: &FamilyPipelines,
195 points: &[(&Values, &ByOp)],
196) -> Result<(), String> {
197 let implemented: BTreeSet<&str> = ops.iter().map(|o| o.op.as_str()).collect();
198 let roles_ok = |r: LinearRole| {
199 ops.iter()
200 .any(|o| o.op == "linear" && (o.roles.is_empty() || o.roles.contains(&r)))
201 };
202 let check_keys = |by: &ByOp, whose: &str| -> Result<(), String> {
203 for key in by.keys() {
204 let k = base_of(key)?;
205 let listed = implemented.contains(k.op) || implemented.contains(k.base);
206 if !listed || k.role.is_some_and(|r| !roles_ok(r)) {
207 return Err(format!(
208 "{whose} declares a pipeline for `{key}`, which the family does not implement"
209 ));
210 }
211 }
212 Ok(())
213 };
214 check_keys(&decl.family, "the family")?;
215 for o in ops {
218 let covered = if o.op == "linear" && !o.roles.is_empty() {
219 o.roles.iter().all(|r| {
220 decl.family.contains_key(&format!("linear:{}", r.name()))
221 || decl.family.contains_key("linear")
222 })
223 } else {
224 decl.family.contains_key(&o.op)
225 };
226 if !covered {
227 return Err(format!("no `pipeline.{}` for op `{}`", o.op, o.op));
228 }
229 }
230 for (k, by) in &decl.kernels {
231 if !kernels.contains(k) {
232 return Err(format!(
233 "kernel_pipeline names `{k}`, which is not its kernel"
234 ));
235 }
236 check_keys(by, &format!("kernel_pipeline `{k}`"))?;
237 }
238 for (values, own) in points {
239 check_keys(own, &format!("point {values:?}"))?;
240 let mut all: Vec<(&String, &Entry)> = decl.family.iter().collect();
241 all.extend(own.iter());
242 all.extend(decl.kernels.values().flat_map(|b| b.iter()));
243 for (key, e) in all {
244 if let Entry::Pipeline(p) = e {
245 resolve(p, values)
246 .map_err(|e| format!("pipeline `{key}` at point {values:?}: {e}"))?;
247 }
248 }
249 }
250 Ok(())
251}
252
253fn substitute(text: &str, values: &Values) -> Result<String, String> {
255 let mut out = String::with_capacity(text.len());
256 let mut rest = text;
257 while let Some(at) = rest.find('{') {
258 out.push_str(&rest[..at]);
259 let end = rest[at..]
260 .find('}')
261 .ok_or_else(|| format!("`{text}` opens a `{{` it does not close"))?;
262 let name = &rest[at + 1..at + end];
263 let v = values
264 .get(name)
265 .ok_or_else(|| format!("`{{{name}}}` is no parameter of the point"))?;
266 out.push_str(v);
267 rest = &rest[at + end + 1..];
268 }
269 out.push_str(rest);
270 Ok(out)
271}
272
273pub fn resolve(p: &RawPipeline, values: &Values) -> Result<NodePipeline, String> {
275 let fmts = |list: &[String]| -> Result<Vec<Format>, String> {
276 list.iter()
277 .map(|s| Format::parse(&substitute(s, values)?).map_err(|e| e.to_string()))
278 .collect()
279 };
280 let mut steps = Vec::with_capacity(p.steps.len());
281 for (kind, text) in &p.steps {
282 let value = parse_value(*kind, &substitute(text, values)?)
283 .map_err(|e| format!("`{}`: {e}", step_name(*kind)))?;
284 steps.push(Step { kind: *kind, value });
285 }
286 Ok(NodePipeline {
287 inputs: fmts(&p.inputs)?,
288 steps,
289 outputs: fmts(&p.outputs)?,
290 })
291}
292
293#[derive(Debug, Clone, Copy)]
296pub struct Site<'a> {
297 pub node: &'a Node,
299 pub after: Option<&'a OpKind>,
301 pub feeds: Option<&'a OpKind>,
303}
304
305impl<'a> Site<'a> {
306 pub fn of(c: &'a crate::ir::Circuit, n: crate::ir::NodeIdx) -> Self {
308 let node = &c.nodes[n];
309 let after = node
310 .inputs
311 .first()
312 .and_then(|&e| c.edges[e].producer)
313 .map(|p| &c.nodes[p].op);
314 let feeds = node
315 .outputs
316 .first()
317 .and_then(|&e| c.edges[e].consumers.first())
318 .map(|&k| &c.nodes[k].op);
319 Site { node, after, feeds }
320 }
321}
322
323fn names(op: &OpKind) -> Vec<String> {
324 match op {
325 OpKind::Linear(_) | OpKind::ActQuant(_) => vec![op.name(), op.base_name().to_string()],
326 _ => vec![op.base_name().to_string()],
327 }
328}
329
330fn lookup<'a>(by: &'a ByOp, site: &Site<'_>) -> Option<&'a Entry> {
333 let around = |o: Option<&OpKind>| o.map(names).unwrap_or_default();
334 let (after, feeds) = (around(site.after), around(site.feeds));
335 for op in names(&site.node.op) {
336 let mut keys = Vec::new();
337 for a in &after {
338 for f in &feeds {
339 keys.push(format!("{op} after {a} feeds {f}"));
340 }
341 }
342 keys.extend(feeds.iter().map(|f| format!("{op} feeds {f}")));
343 keys.extend(after.iter().map(|a| format!("{op} after {a}")));
344 keys.push(op.clone());
345 if let Some(e) = keys.iter().find_map(|k| by.get(k)) {
346 return Some(e);
347 }
348 }
349 None
350}
351
352pub fn declared_for(
356 decl: &FamilyPipelines,
357 points: &[(&Values, &ByOp)],
358 group: &[KernelId],
359 site: &Site<'_>,
360 point: &Values,
361) -> Result<NodePipeline, String> {
362 let op = site.node.op.name();
363 let matching: Vec<&(&Values, &ByOp)> = points
364 .iter()
365 .filter(|(v, _)| point.iter().all(|(k, x)| v.get(k) == Some(x)))
366 .collect();
367 if matching.is_empty() {
368 return Err(format!(
369 "the node's point {point:?} is not one the family instantiates"
370 ));
371 }
372 let by_kernel: BTreeSet<&Entry> = group
373 .iter()
374 .filter_map(|k| decl.kernels.get(k))
375 .filter_map(|by| lookup(by, site))
376 .collect();
377 let chosen = match by_kernel.into_iter().collect::<Vec<_>>().as_slice() {
378 [] => {
379 let own: BTreeSet<Option<&Entry>> =
380 matching.iter().map(|(_, by)| lookup(by, site)).collect();
381 match own.into_iter().collect::<Vec<_>>().as_slice() {
382 [Some(p)] => *p,
383 [None] => lookup(&decl.family, site)
384 .ok_or_else(|| format!("no pipeline is declared for `{op}`"))?,
385 _ => {
386 return Err(format!(
387 "the points matching {point:?} declare different pipelines for `{op}`"
388 ));
389 }
390 }
391 }
392 [one] => *one,
393 _ => {
394 return Err(format!(
395 "the group's kernels declare different pipelines for `{op}`"
396 ));
397 }
398 };
399 match chosen {
400 Entry::Pipeline(p) => resolve(p, point),
401 Entry::Uninstantiated => Err(format!(
402 "the family lists `{op}` as a parameterization target that no source implements"
403 )),
404 }
405}
406
407#[cfg(test)]
408#[path = "declare_tests.rs"]
409mod declare_tests;