1use sim_kernel::{Cx, Error, Expr, NumberLiteral, Result, Symbol, Value};
4use sim_lib_compute_model::ModeledComputeProfile;
5
6use crate::{
7 ComputeDeviceIdentity, ComputeEvidenceKind, ComputeProfileLimits, ComputeProfileProvenance,
8 ComputeProfileSamples, ComputeThermalPowerContext, MeasuredComputeProfile,
9};
10
11const PROFILE_SCHEMA: &str = "sim.compute.auto.profile.v2";
12const LEGACY_PROFILE_SCHEMA: &str = "sim.compute.auto.profile.v1";
13const DEFAULT_MAX_KEY_BYTES: usize = 96;
14const DEFAULT_MAX_PROFILE_BYTES: usize = 8192;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct ProfileStorePolicy {
19 pub max_key_bytes: usize,
21 pub max_profile_bytes: usize,
23}
24
25impl Default for ProfileStorePolicy {
26 fn default() -> Self {
27 Self {
28 max_key_bytes: DEFAULT_MAX_KEY_BYTES,
29 max_profile_bytes: DEFAULT_MAX_PROFILE_BYTES,
30 }
31 }
32}
33
34pub struct ProfileStore {
36 table: Value,
37 policy: ProfileStorePolicy,
38}
39
40impl ProfileStore {
41 pub fn new(table: Value, policy: ProfileStorePolicy) -> Result<Self> {
43 if table.object().as_table_impl().is_none() {
44 return Err(Error::TypeMismatch {
45 expected: "Table or Dir",
46 found: "non-table",
47 });
48 }
49 Ok(Self { table, policy })
50 }
51
52 pub fn save(&self, cx: &mut Cx, key: Symbol, profile: &MeasuredComputeProfile) -> Result<()> {
54 self.validate_key(&key)?;
55 if profile.estimated_profile_bytes() > self.policy.max_profile_bytes {
56 return Err(Error::Eval(
57 "compute profile exceeds byte policy".to_owned(),
58 ));
59 }
60 let value = profile_to_value(cx, profile)?;
61 self.table
62 .object()
63 .as_table_impl()
64 .expect("profile store table")
65 .set(cx, key, value)
66 }
67
68 pub fn load(&self, cx: &mut Cx, key: Symbol) -> Result<Option<MeasuredComputeProfile>> {
70 self.validate_key(&key)?;
71 let table = self
72 .table
73 .object()
74 .as_table_impl()
75 .expect("profile store table");
76 if !table.has(cx, key.clone())? {
77 return Ok(None);
78 }
79 let value = table.get(cx, key)?;
80 let profile = profile_from_value(cx, &value)?;
81 if profile.estimated_profile_bytes() > self.policy.max_profile_bytes {
82 return Err(Error::Eval(
83 "stored compute profile exceeds byte policy".to_owned(),
84 ));
85 }
86 Ok(Some(profile))
87 }
88
89 pub fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
91 let keys = self
92 .table
93 .object()
94 .as_table_impl()
95 .expect("profile store table")
96 .keys(cx)?;
97 for key in &keys {
98 self.validate_key(key)?;
99 }
100 Ok(keys)
101 }
102
103 fn validate_key(&self, key: &Symbol) -> Result<()> {
104 if key.to_string().len() > self.policy.max_key_bytes {
105 return Err(Error::Eval(
106 "compute profile key exceeds byte policy".to_owned(),
107 ));
108 }
109 Ok(())
110 }
111}
112
113fn profile_to_value(cx: &mut Cx, profile: &MeasuredComputeProfile) -> Result<Value> {
114 let entries = vec![
115 (sym("schema"), string(cx, PROFILE_SCHEMA)?),
116 (sym("adapter"), string(cx, &profile.identity.adapter)?),
117 (sym("driver"), string(cx, &profile.identity.driver)?),
118 (sym("backend"), string(cx, &profile.identity.backend)?),
119 (
120 sym("max-resident-bytes"),
121 number(cx, profile.limits.max_resident_bytes)?,
122 ),
123 (
124 sym("max-storage-binding-bytes"),
125 number(cx, profile.limits.max_storage_binding_bytes)?,
126 ),
127 (
128 sym("max-queue-depth"),
129 number(cx, profile.limits.max_queue_depth as u64)?,
130 ),
131 (
132 sym("max-queue-bytes"),
133 number(cx, profile.limits.max_queue_bytes)?,
134 ),
135 (
136 sym("submission-deadline-ticks"),
137 number(cx, profile.limits.submission_deadline_ticks)?,
138 ),
139 (
140 sym("upload-bytes-per-tick"),
141 number_list(cx, &profile.samples.upload_bytes_per_tick)?,
142 ),
143 (
144 sym("download-bytes-per-tick"),
145 number_list(cx, &profile.samples.download_bytes_per_tick)?,
146 ),
147 (
148 sym("launch-ticks"),
149 number_list(cx, &profile.samples.launch_ticks)?,
150 ),
151 (
152 sym("element-elements-per-tick"),
153 number_list(cx, &profile.samples.element_elements_per_tick)?,
154 ),
155 (
156 sym("reduction-elements-per-tick"),
157 number_list(cx, &profile.samples.reduction_elements_per_tick)?,
158 ),
159 (
160 sym("matmul-ops-per-tick"),
161 number_list(cx, &profile.samples.matmul_ops_per_tick)?,
162 ),
163 (sym("thermal"), string(cx, &profile.context.thermal)?),
164 (sym("power"), string(cx, &profile.context.power)?),
165 (sym("tile-bytes"), number(cx, profile.tile_bytes)?),
166 (
167 sym("allocation-bytes"),
168 number_list(cx, &profile.allocation_bytes)?,
169 ),
170 (sym("producer"), string(cx, &profile.provenance.producer)?),
171 (
172 sym("measured-at-tick"),
173 number(cx, profile.provenance.measured_at_tick)?,
174 ),
175 (
176 sym("stale-after-ticks"),
177 number(cx, profile.provenance.stale_after_ticks)?,
178 ),
179 (
180 sym("modeled-provider"),
181 string(cx, &profile.modeled.provider)?,
182 ),
183 (
184 sym("evidence-kind"),
185 string(cx, profile.provenance.evidence_kind.as_str())?,
186 ),
187 ];
188 let mut entries = entries;
189 if let Some(observed) = &profile.provenance.observed_identity {
190 entries.extend([
191 (sym("observed-adapter"), string(cx, &observed.adapter)?),
192 (sym("observed-driver"), string(cx, &observed.driver)?),
193 (sym("observed-backend"), string(cx, &observed.backend)?),
194 ]);
195 }
196 cx.factory().table(entries)
197}
198
199fn profile_from_value(cx: &mut Cx, value: &Value) -> Result<MeasuredComputeProfile> {
200 let entries = value
201 .object()
202 .as_table_impl()
203 .ok_or(Error::TypeMismatch {
204 expected: "profile table",
205 found: "non-table",
206 })?
207 .entries(cx)?;
208 let get = |name: &str| -> Result<Value> {
209 entries
210 .iter()
211 .find(|(key, _)| *key == sym(name))
212 .map(|(_, value)| value.clone())
213 .ok_or_else(|| Error::Eval(format!("compute profile missing {name}")))
214 };
215 let schema = string_value(cx, &get("schema")?)?;
216 if schema != PROFILE_SCHEMA && schema != LEGACY_PROFILE_SCHEMA {
217 return Err(Error::Eval("unsupported compute profile schema".to_owned()));
218 }
219 let max_queue_depth = number_value(cx, &get("max-queue-depth")?)?;
220 let identity = ComputeDeviceIdentity {
221 adapter: string_value(cx, &get("adapter")?)?,
222 driver: string_value(cx, &get("driver")?)?,
223 backend: string_value(cx, &get("backend")?)?,
224 };
225 let evidence_kind = match maybe_string(cx, &entries, "evidence-kind")? {
226 Some(kind) => ComputeEvidenceKind::try_from(kind.as_str())
227 .map_err(|err| Error::Eval(err.to_string()))?,
228 None => ComputeEvidenceKind::Modeled,
229 };
230 let observed_identity = match (
231 maybe_string(cx, &entries, "observed-adapter")?,
232 maybe_string(cx, &entries, "observed-driver")?,
233 maybe_string(cx, &entries, "observed-backend")?,
234 ) {
235 (Some(adapter), Some(driver), Some(backend)) => {
236 Some(ComputeDeviceIdentity::new(adapter, driver, backend))
237 }
238 (None, None, None) => None,
239 _ => {
240 return Err(Error::Eval(
241 "compute profile has partial observed identity".to_owned(),
242 ));
243 }
244 };
245 Ok(MeasuredComputeProfile {
246 identity,
247 limits: ComputeProfileLimits {
248 max_resident_bytes: number_value(cx, &get("max-resident-bytes")?)?,
249 max_storage_binding_bytes: number_value(cx, &get("max-storage-binding-bytes")?)?,
250 max_queue_depth: usize::try_from(max_queue_depth)
251 .map_err(|_| Error::Eval("max-queue-depth exceeds usize".to_owned()))?,
252 max_queue_bytes: number_value(cx, &get("max-queue-bytes")?)?,
253 submission_deadline_ticks: number_value(cx, &get("submission-deadline-ticks")?)?,
254 },
255 samples: ComputeProfileSamples {
256 upload_bytes_per_tick: number_vec(cx, &get("upload-bytes-per-tick")?)?,
257 download_bytes_per_tick: number_vec(cx, &get("download-bytes-per-tick")?)?,
258 launch_ticks: number_vec(cx, &get("launch-ticks")?)?,
259 element_elements_per_tick: number_vec(cx, &get("element-elements-per-tick")?)?,
260 reduction_elements_per_tick: number_vec(cx, &get("reduction-elements-per-tick")?)?,
261 matmul_ops_per_tick: number_vec(cx, &get("matmul-ops-per-tick")?)?,
262 },
263 context: ComputeThermalPowerContext {
264 thermal: string_value(cx, &get("thermal")?)?,
265 power: string_value(cx, &get("power")?)?,
266 },
267 tile_bytes: number_value(cx, &get("tile-bytes")?)?,
268 allocation_bytes: number_vec(cx, &get("allocation-bytes")?)?,
269 provenance: ComputeProfileProvenance {
270 evidence_kind,
271 producer: string_value(cx, &get("producer")?)?,
272 measured_at_tick: number_value(cx, &get("measured-at-tick")?)?,
273 stale_after_ticks: number_value(cx, &get("stale-after-ticks")?)?,
274 observed_identity,
275 },
276 modeled: ModeledComputeProfile {
277 provider: string_value(cx, &get("modeled-provider")?)?,
278 max_queue_depth: usize::try_from(max_queue_depth)
279 .map_err(|_| Error::Eval("max-queue-depth exceeds usize".to_owned()))?,
280 max_queue_bytes: number_value(cx, &get("max-queue-bytes")?)?,
281 max_resident_bytes: number_value(cx, &get("max-resident-bytes")?)?,
282 segment_tile_bytes: number_value(cx, &get("tile-bytes")?)?,
283 max_storage_binding_bytes: number_value(cx, &get("max-storage-binding-bytes")?)?,
284 submission_deadline_ticks: number_value(cx, &get("submission-deadline-ticks")?)?,
285 fault: None,
286 auto_flush_batches: false,
287 },
288 })
289}
290
291fn maybe_string(cx: &mut Cx, entries: &[(Symbol, Value)], name: &str) -> Result<Option<String>> {
292 entries
293 .iter()
294 .find(|(key, _)| *key == sym(name))
295 .map(|(_, value)| string_value(cx, value))
296 .transpose()
297}
298
299fn number(cx: &mut Cx, value: u64) -> Result<Value> {
300 cx.factory()
301 .number_literal(Symbol::qualified("numbers", "u64"), value.to_string())
302}
303
304fn string(cx: &mut Cx, value: &str) -> Result<Value> {
305 cx.factory().string(value.to_owned())
306}
307
308fn number_list(cx: &mut Cx, values: &[u64]) -> Result<Value> {
309 let values = values
310 .iter()
311 .map(|value| number(cx, *value))
312 .collect::<Result<Vec<_>>>()?;
313 cx.factory().list(values)
314}
315
316fn string_value(cx: &mut Cx, value: &Value) -> Result<String> {
317 match value.object().as_expr(cx)? {
318 Expr::String(value) => Ok(value),
319 _ => Err(Error::TypeMismatch {
320 expected: "string",
321 found: "non-string",
322 }),
323 }
324}
325
326fn number_value(cx: &mut Cx, value: &Value) -> Result<u64> {
327 match value.object().as_expr(cx)? {
328 Expr::Number(NumberLiteral { canonical, .. }) => canonical
329 .parse::<u64>()
330 .map_err(|_| Error::Eval("invalid u64 profile value".to_owned())),
331 _ => Err(Error::TypeMismatch {
332 expected: "number",
333 found: "non-number",
334 }),
335 }
336}
337
338fn number_vec(cx: &mut Cx, value: &Value) -> Result<Vec<u64>> {
339 match value.object().as_expr(cx)? {
340 Expr::List(values) => values
341 .iter()
342 .map(|expr| match expr {
343 Expr::Number(NumberLiteral { canonical, .. }) => canonical
344 .parse::<u64>()
345 .map_err(|_| Error::Eval("invalid u64 profile sample".to_owned())),
346 _ => Err(Error::TypeMismatch {
347 expected: "number list",
348 found: "non-number list",
349 }),
350 })
351 .collect(),
352 _ => Err(Error::TypeMismatch {
353 expected: "list",
354 found: "non-list",
355 }),
356 }
357}
358
359fn sym(name: &str) -> Symbol {
360 Symbol::qualified("compute-profile", name)
361}