1use std::collections::BTreeMap;
18
19use serde::Deserialize;
20
21use crate::format::Format;
22use crate::ir::{OpKind, Section};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CopyLaw {
27 Weight(Format),
29 RowScaleF32,
31 BlockGridF32(u32, u32),
33}
34
35impl CopyLaw {
36 pub fn parse(s: &str) -> Option<Self> {
39 if s == "row_scale_f32" {
40 return Some(Self::RowScaleF32);
41 }
42 if let Some(rc) = s.strip_prefix("block_grid_f32/") {
43 let (r, c) = rc.split_once('x')?;
44 let pos = |v: &str| v.parse::<u32>().ok().filter(|&v| v > 0);
45 return Some(Self::BlockGridF32(pos(r)?, pos(c)?));
46 }
47 Format::parse(s).ok().map(Self::Weight)
48 }
49
50 pub fn bytes(self, out: u64, k: u64) -> Option<u64> {
53 match self {
54 Self::Weight(f) => f.weight_bytes(out, k),
55 Self::RowScaleF32 => out.checked_mul(4),
56 Self::BlockGridF32(r, c) => out
57 .div_ceil(u64::from(r))
58 .checked_mul(k.div_ceil(u64::from(c)))?
59 .checked_mul(4),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq)]
66pub struct CopyRule {
67 pub id: String,
69 pub arch: Vec<String>,
71 pub ops: Vec<String>,
73 pub section: Section,
75 pub stored: Vec<Format>,
77 pub served: Vec<Format>,
79 pub when: BTreeMap<String, String>,
81 pub law: CopyLaw,
83 pub count: u64,
85 pub leaked: bool,
87 pub site: String,
89}
90
91#[derive(Deserialize)]
92#[serde(deny_unknown_fields)]
93struct File {
94 schema: u32,
95 copy: Vec<RuleFile>,
96}
97
98#[derive(Deserialize)]
99#[serde(deny_unknown_fields)]
100struct RuleFile {
101 id: String,
102 arch: Vec<String>,
103 ops: Vec<String>,
104 section: String,
105 #[serde(default)]
106 stored: Vec<String>,
107 #[serde(default)]
108 served: Vec<String>,
109 #[serde(default)]
110 when: BTreeMap<String, String>,
111 law: String,
112 count: u64,
113 #[serde(default)]
114 leaked: bool,
115 site: String,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
120#[error("COPIES.toml: {0}")]
121pub struct CopyError(pub String);
122
123pub const COPY_SETTINGS: [&str; 6] = [
128 "speculative",
129 "weight_quantization",
130 "lm_head_dtype",
131 "expert_quantization",
132 "latent_moe",
133 "moe_expert_tables",
134];
135
136pub fn parse_copies(text: &str) -> Result<Vec<CopyRule>, CopyError> {
138 let file: File = toml::from_str(text).map_err(|e| CopyError(e.to_string()))?;
139 if file.schema != 1 {
140 return Err(CopyError(format!(
141 "schema {} (this build reads 1)",
142 file.schema
143 )));
144 }
145 let mut out: Vec<CopyRule> = Vec::with_capacity(file.copy.len());
146 for r in file.copy {
147 let bad = |what: String| CopyError(format!("copy `{}`: {what}", r.id));
148 if out.iter().any(|o| o.id == r.id) {
149 return Err(bad("listed twice".into()));
150 }
151 for op in &r.ops {
152 let (name, q) = op
153 .split_once(':')
154 .map_or((op.as_str(), None), |(a, b)| (a, Some(b)));
155 let parsed = match name {
156 "linear" => OpKind::parse(name, q, None),
157 _ if q.is_some() => return Err(bad(format!("op `{op}` takes no qualifier"))),
158 _ => OpKind::parse(name, None, None),
159 };
160 if !parsed.is_ok_and(|k| k.reads_linear_weight()) {
161 return Err(bad(format!("op `{op}` is no weight-reading op")));
162 }
163 }
164 let formats = |v: &[String]| -> Result<Vec<Format>, CopyError> {
165 v.iter()
166 .map(|f| Format::parse(f).map_err(|e| bad(e.to_string())))
167 .collect()
168 };
169 for k in r.when.keys() {
170 if !COPY_SETTINGS.contains(&k.as_str()) {
171 return Err(bad(format!(
172 "setting `{k}` is not one of {}",
173 COPY_SETTINGS.join(", ")
174 )));
175 }
176 }
177 let section = match r.section.as_str() {
178 "main" => Section::Main,
179 "draft" => Section::Draft,
180 s => return Err(bad(format!("section `{s}` is not main or draft"))),
181 };
182 if r.count == 0 || r.arch.is_empty() || r.ops.is_empty() || r.site.is_empty() {
183 return Err(bad("needs arch, ops, a site and a positive count".into()));
184 }
185 out.push(CopyRule {
186 law: CopyLaw::parse(&r.law).ok_or_else(|| bad(format!("law `{}`", r.law)))?,
187 stored: formats(&r.stored)?,
188 served: formats(&r.served)?,
189 id: r.id,
190 arch: r.arch,
191 ops: r.ops,
192 section,
193 when: r.when,
194 count: r.count,
195 leaked: r.leaked,
196 site: r.site,
197 });
198 }
199 Ok(out)
200}
201
202impl CopyRule {
203 pub fn matches(
206 &self,
207 arch: &str,
208 op: &OpKind,
209 section: Section,
210 stored: Format,
211 served: Format,
212 settings: &BTreeMap<String, String>,
213 ) -> bool {
214 self.arch.iter().any(|a| a == arch)
215 && self.section == section
216 && self.ops.iter().any(|o| *o == op.name())
217 && (self.stored.is_empty() || self.stored.contains(&stored))
218 && (self.served.is_empty() || self.served.contains(&served))
219 && self.when.iter().all(|(k, v)| settings.get(k) == Some(v))
220 }
221}