1use crate::attributes::{Attribute, Attributes};
22use crate::context::Context;
23use crate::functions::FunctionData;
24use crate::location::Location;
25use crate::types::{Dimension, MemoryLayout};
26
27pub use crate::types::DataType;
30
31#[derive(Debug, Clone)]
33pub struct Shape(pub Vec<usize>);
34
35impl Shape {
36 pub fn new(dims: &[usize]) -> Self {
37 Self(dims.to_vec())
38 }
39}
40
41#[derive(Debug, Clone)]
43pub enum ModelType {
44 Tensor { shape: Vec<usize>, dtype: DataType },
45 Qubit,
46 Bit,
47 Integer { bits: u32 },
48}
49
50pub fn tensor(shape: &[usize], dtype: DataType) -> ModelType {
52 ModelType::Tensor {
53 shape: shape.to_vec(),
54 dtype,
55 }
56}
57
58pub fn tensor_2d(m: usize, n: usize, dtype: DataType) -> ModelType {
60 ModelType::Tensor {
61 shape: vec![m, n],
62 dtype,
63 }
64}
65
66pub fn tensor_3d(b: usize, s: usize, d: usize, dtype: DataType) -> ModelType {
68 ModelType::Tensor {
69 shape: vec![b, s, d],
70 dtype,
71 }
72}
73
74pub fn tensor_4d(b: usize, c: usize, h: usize, w: usize, dtype: DataType) -> ModelType {
76 ModelType::Tensor {
77 shape: vec![b, c, h, w],
78 dtype,
79 }
80}
81
82pub fn tensor_1d(n: usize, dtype: DataType) -> ModelType {
84 ModelType::Tensor {
85 shape: vec![n],
86 dtype,
87 }
88}
89
90#[derive(Debug, Clone)]
92struct OpDef {
93 op_name: String,
94 inputs: Vec<String>,
95 result_name: String,
96 result_type: ModelType,
97 attrs: Vec<(String, Attribute)>,
98}
99
100#[derive(Debug)]
102pub struct FunctionBuilder {
103 name: String,
104 params: Vec<(String, ModelType)>,
105 ops: Vec<OpDef>,
106 return_name: Option<String>,
107 dialect: String,
108}
109
110impl FunctionBuilder {
111 fn new(name: &str) -> Self {
112 Self {
113 name: name.to_string(),
114 params: Vec::new(),
115 ops: Vec::new(),
116 return_name: None,
117 dialect: "tensor".to_string(),
118 }
119 }
120
121 pub fn param(mut self, name: &str, ty: ModelType) -> Self {
123 self.params.push((name.to_string(), ty));
124 self
125 }
126
127 pub fn op(
129 mut self,
130 op_name: &str,
131 inputs: &[&str],
132 result: &str,
133 result_type: ModelType,
134 ) -> Self {
135 self.ops.push(OpDef {
136 op_name: op_name.to_string(),
137 inputs: inputs.iter().map(|s| s.to_string()).collect(),
138 result_name: result.to_string(),
139 result_type,
140 attrs: Vec::new(),
141 });
142 self
143 }
144
145 pub fn op_with_attrs(
147 mut self,
148 op_name: &str,
149 inputs: &[&str],
150 result: &str,
151 result_type: ModelType,
152 attrs: Vec<(&str, Attribute)>,
153 ) -> Self {
154 self.ops.push(OpDef {
155 op_name: op_name.to_string(),
156 inputs: inputs.iter().map(|s| s.to_string()).collect(),
157 result_name: result.to_string(),
158 result_type,
159 attrs: attrs.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
160 });
161 self
162 }
163
164 pub fn returns(mut self, name: &str) -> Self {
166 self.return_name = Some(name.to_string());
167 self
168 }
169
170 pub fn dialect(mut self, d: &str) -> Self {
172 self.dialect = d.to_string();
173 self
174 }
175}
176
177#[derive(Debug)]
179pub struct ModelBuilder {
180 module_name: String,
181 functions: Vec<FunctionBuilder>,
182 dialect_directive: String,
183}
184
185impl ModelBuilder {
186 pub fn new(name: &str) -> Self {
188 Self {
189 module_name: name.to_string(),
190 functions: Vec::new(),
191 dialect_directive: "tensor".to_string(),
192 }
193 }
194
195 pub fn dialect(mut self, d: &str) -> Self {
197 self.dialect_directive = d.to_string();
198 self
199 }
200
201 pub fn function(mut self, name: &str) -> FunctionBuilderHandle {
203 let fb = FunctionBuilder::new(name);
204 let idx = self.functions.len();
205 self.functions.push(fb);
206 FunctionBuilderHandle {
207 model: self,
208 func_idx: idx,
209 }
210 }
211
212 pub fn build_context(&self) -> Context {
214 let mut ctx = Context::new();
215 let mod_idx = ctx.create_module(&self.module_name);
216
217 for fb in &self.functions {
218 let func_data = self.build_function_data(&mut ctx, fb);
219 ctx.add_function_to_module(mod_idx, func_data);
220 }
221
222 ctx
223 }
224
225 pub fn build_lif(&self) -> String {
227 let mut out = format!("#dialect {}\n\n", self.dialect_directive);
228 out.push_str(&format!("module @{} {{\n\n", self.module_name));
229
230 for fb in &self.functions {
231 out.push_str(&self.emit_function_source(fb));
232 out.push('\n');
233 }
234
235 out.push_str("}\n");
236 out
237 }
238
239 pub fn write_lif(&self, path: &str) -> std::io::Result<()> {
241 let lif = self.build_lif();
242 std::fs::write(path, lif)
243 }
244
245 fn emit_function_source(&self, fb: &FunctionBuilder) -> String {
246 let mut out = String::new();
247
248 out.push_str(&format!(" func @{}(", fb.name));
250 for (i, (pname, pty)) in fb.params.iter().enumerate() {
251 if i > 0 {
252 out.push_str(", ");
253 }
254 out.push_str(&format!("%{}: {}", pname, format_model_type(pty)));
255 }
256 out.push(')');
257
258 if let Some(ref ret_name) = fb.return_name {
260 if let Some(op) = fb.ops.iter().find(|o| o.result_name == *ret_name) {
261 out.push_str(&format!(" -> {}", format_model_type(&op.result_type)));
262 } else if let Some((_, ty)) = fb.params.iter().find(|(n, _)| n == ret_name) {
263 out.push_str(&format!(" -> {}", format_model_type(ty)));
264 }
265 }
266
267 out.push_str(" {\n");
268
269 for op_def in &fb.ops {
271 let inputs: String = op_def
272 .inputs
273 .iter()
274 .map(|n| format!("%{}", n))
275 .collect::<Vec<_>>()
276 .join(", ");
277
278 let input_types: String = op_def
279 .inputs
280 .iter()
281 .filter_map(|n| {
282 if let Some((_, ty)) = fb.params.iter().find(|(pn, _)| pn == n) {
284 Some(format_model_type(ty))
285 } else {
286 fb.ops
287 .iter()
288 .find(|o| o.result_name == *n)
289 .map(|prev_op| format_model_type(&prev_op.result_type))
290 }
291 })
292 .collect::<Vec<_>>()
293 .join(", ");
294
295 let result_type = format_model_type(&op_def.result_type);
296
297 out.push_str(&format!(
298 " %{} = \"{}\"({}) : ({}) -> {}\n",
299 op_def.result_name, op_def.op_name, inputs, input_types, result_type
300 ));
301 }
302
303 if let Some(ref ret_name) = fb.return_name {
305 out.push_str(&format!(" return %{}\n", ret_name));
306 }
307
308 out.push_str(" }\n");
309 out
310 }
311
312 fn build_function_data(&self, ctx: &mut Context, fb: &FunctionBuilder) -> FunctionData {
313 let name_id = ctx.intern_string(&fb.name);
314
315 let param_types: Vec<_> = fb
317 .params
318 .iter()
319 .map(|(_, ty)| self.resolve_model_type(ctx, ty))
320 .collect();
321
322 let return_types = if let Some(ref ret_name) = fb.return_name {
324 if let Some(op) = fb.ops.iter().find(|o| o.result_name == *ret_name) {
326 vec![self.resolve_model_type(ctx, &op.result_type)]
327 } else if let Some((_, ty)) = fb.params.iter().find(|(n, _)| n == ret_name) {
328 vec![self.resolve_model_type(ctx, ty)]
329 } else {
330 vec![]
331 }
332 } else {
333 vec![]
334 };
335
336 let mut func_data = FunctionData::new(name_id, param_types.clone(), return_types);
337
338 let region = ctx.create_region();
340 let block = ctx.create_block();
341 ctx.add_block_to_region(region, block);
342
343 let mut name_map = std::collections::HashMap::new();
345 for (i, (pname, _)) in fb.params.iter().enumerate() {
346 let val = ctx.create_block_arg(block, param_types[i]);
347 name_map.insert(pname.clone(), val);
348 }
349
350 for op_def in &fb.ops {
352 let inputs: Vec<_> = op_def
353 .inputs
354 .iter()
355 .filter_map(|name| name_map.get(name).copied())
356 .collect();
357
358 let result_ty = self.resolve_model_type(ctx, &op_def.result_type);
359
360 let (dialect, _) = op_def
361 .op_name
362 .split_once('.')
363 .unwrap_or(("core", &op_def.op_name));
364
365 let mut attrs = Attributes::new();
366 for (key, val) in &op_def.attrs {
367 attrs.set(key.clone(), val.clone());
368 }
369
370 let (op_key, results) = ctx.create_op(
371 &op_def.op_name,
372 dialect,
373 inputs,
374 vec![result_ty],
375 attrs,
376 Location::unknown(),
377 );
378 ctx.add_op_to_block(block, op_key);
379
380 if !results.is_empty() {
381 name_map.insert(op_def.result_name.clone(), results[0]);
382 }
383 }
384
385 if let Some(ref ret_name) = fb.return_name {
387 if let Some(&ret_val) = name_map.get(ret_name) {
388 let (ret_op, _) = ctx.create_op(
389 "core.return",
390 "core",
391 vec![ret_val],
392 vec![],
393 Attributes::new(),
394 Location::unknown(),
395 );
396 ctx.add_op_to_block(block, ret_op);
397 }
398 }
399
400 func_data.body = Some(region);
401 func_data
402 }
403
404 fn resolve_model_type(&self, ctx: &mut Context, mt: &ModelType) -> crate::types::TypeId {
405 match mt {
406 ModelType::Tensor { shape, dtype } => {
407 let dims: Vec<Dimension> = shape.iter().map(|&d| Dimension::Constant(d)).collect();
408 ctx.make_tensor_type(dims, *dtype, MemoryLayout::Contiguous)
409 }
410 ModelType::Qubit => ctx.make_qubit_type(),
411 ModelType::Bit => ctx.make_bit_type(),
412 ModelType::Integer { bits } => ctx.make_integer_type(*bits, true),
413 }
414 }
415}
416
417pub struct FunctionBuilderHandle {
419 model: ModelBuilder,
420 func_idx: usize,
421}
422
423impl FunctionBuilderHandle {
424 pub fn param(mut self, name: &str, ty: ModelType) -> Self {
425 let fb = std::mem::replace(
426 &mut self.model.functions[self.func_idx],
427 FunctionBuilder::new("__tmp__"),
428 );
429 self.model.functions[self.func_idx] = fb.param(name, ty);
430 self
431 }
432
433 pub fn op(
434 mut self,
435 op_name: &str,
436 inputs: &[&str],
437 result: &str,
438 result_type: ModelType,
439 ) -> Self {
440 let fb = std::mem::replace(
441 &mut self.model.functions[self.func_idx],
442 FunctionBuilder::new("__tmp__"),
443 );
444 self.model.functions[self.func_idx] = fb.op(op_name, inputs, result, result_type);
445 self
446 }
447
448 pub fn op_with_attrs(
449 mut self,
450 op_name: &str,
451 inputs: &[&str],
452 result: &str,
453 result_type: ModelType,
454 attrs: Vec<(&str, Attribute)>,
455 ) -> Self {
456 let fb = std::mem::replace(
457 &mut self.model.functions[self.func_idx],
458 FunctionBuilder::new("__tmp__"),
459 );
460 self.model.functions[self.func_idx] =
461 fb.op_with_attrs(op_name, inputs, result, result_type, attrs);
462 self
463 }
464
465 pub fn returns(mut self, name: &str) -> Self {
466 let fb = std::mem::replace(
467 &mut self.model.functions[self.func_idx],
468 FunctionBuilder::new("__tmp__"),
469 );
470 self.model.functions[self.func_idx] = fb.returns(name);
471 self
472 }
473
474 pub fn dialect(mut self, d: &str) -> Self {
475 let fb = std::mem::replace(
476 &mut self.model.functions[self.func_idx],
477 FunctionBuilder::new("__tmp__"),
478 );
479 self.model.functions[self.func_idx] = fb.dialect(d);
480 self
481 }
482
483 pub fn done(self) -> ModelBuilder {
485 self.model
486 }
487}
488
489fn format_model_type(mt: &ModelType) -> String {
491 match mt {
492 ModelType::Tensor { shape, dtype } => {
493 let dims: String = shape
494 .iter()
495 .map(|d| d.to_string())
496 .collect::<Vec<_>>()
497 .join("x");
498 let dt = match dtype {
499 DataType::FP64 => "f64",
500 DataType::FP32 => "f32",
501 DataType::FP16 => "f16",
502 DataType::BF16 => "bf16",
503 DataType::FP8E4M3 => "f8e4m3",
504 DataType::FP8E5M2 => "f8e5m2",
505 DataType::INT64 => "i64",
506 DataType::INT32 => "i32",
507 DataType::INT16 => "i16",
508 DataType::INT8 => "i8",
509 DataType::INT4 => "i4",
510 DataType::INT2 => "i2",
511 DataType::UINT8 => "ui8",
512 DataType::Bool => "i1",
513 DataType::Index => "index",
514 };
515 format!("tensor<{}x{}>", dims, dt)
516 }
517 ModelType::Qubit => "qubit".to_string(),
518 ModelType::Bit => "bit".to_string(),
519 ModelType::Integer { bits } => format!("i{}", bits),
520 }
521}
522
523pub fn build_lith_config(
525 backend: &str,
526 device: &str,
527 precision: &str,
528 passes: &[&str],
529 max_flops: Option<u64>,
530 max_memory: Option<u64>,
531) -> String {
532 let mut out = String::new();
533 out.push_str(&format!(
534 "[target]\nbackend = \"{}\"\ndevice = \"{}\"\nprecision = \"{}\"\n\n",
535 backend, device, precision
536 ));
537
538 if max_flops.is_some() || max_memory.is_some() {
539 out.push_str("[budget]\n");
540 if let Some(f) = max_flops {
541 out.push_str(&format!("max_flops = {}\n", f));
542 }
543 if let Some(m) = max_memory {
544 out.push_str(&format!("max_memory_bytes = {}\n", m));
545 }
546 out.push('\n');
547 }
548
549 out.push_str("[optimisation]\nlevel = O3\n");
550 out.push_str(&format!("passes = {}\n", passes.join(", ")));
551 out.push_str("max_iterations = 10\n\n");
552
553 out.push_str("[simulation]\nshape_propagation = true\nflop_counting = true\nmemory_analysis = true\nnoise_simulation = false\n");
554
555 out
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn test_simple_model() {
564 let lif = ModelBuilder::new("test_mlp")
565 .function("forward")
566 .param("x", tensor(&[1, 784], DataType::FP32))
567 .param("w", tensor_2d(784, 256, DataType::FP32))
568 .op(
569 "tensor.matmul",
570 &["x", "w"],
571 "h",
572 tensor(&[1, 256], DataType::FP32),
573 )
574 .op(
575 "tensor.relu",
576 &["h"],
577 "out",
578 tensor(&[1, 256], DataType::FP32),
579 )
580 .returns("out")
581 .done()
582 .build_lif();
583
584 assert!(lif.contains("#dialect tensor"));
585 assert!(lif.contains("module @test_mlp"));
586 assert!(lif.contains("tensor.matmul"));
587 assert!(lif.contains("tensor.relu"));
588 assert!(lif.contains("return %out"));
589 }
590
591 #[test]
592 fn test_build_context() {
593 let ctx = ModelBuilder::new("ctx_test")
594 .function("f")
595 .param("a", tensor(&[4, 4], DataType::FP32))
596 .param("b", tensor(&[4, 4], DataType::FP32))
597 .op(
598 "tensor.add",
599 &["a", "b"],
600 "c",
601 tensor(&[4, 4], DataType::FP32),
602 )
603 .returns("c")
604 .done()
605 .build_context();
606
607 assert_eq!(ctx.modules.len(), 1);
608 assert_eq!(ctx.modules[0].functions.len(), 1);
609 assert!(ctx.ops.len() >= 2); }
611
612 #[test]
613 fn test_write_lif() {
614 let path = "/tmp/lift_builder_test.lif";
615 ModelBuilder::new("write_test")
616 .function("f")
617 .param("x", tensor(&[8], DataType::FP32))
618 .op("tensor.relu", &["x"], "y", tensor(&[8], DataType::FP32))
619 .returns("y")
620 .done()
621 .write_lif(path)
622 .unwrap();
623
624 let content = std::fs::read_to_string(path).unwrap();
625 assert!(content.contains("#dialect tensor"));
626 assert!(content.contains("tensor.relu"));
627 }
628
629 #[test]
630 fn test_lith_config() {
631 let config = build_lith_config(
632 "llvm",
633 "h100",
634 "fp16",
635 &["canonicalize", "dce", "tensor-fusion"],
636 Some(1_000_000_000),
637 Some(80_000_000_000),
638 );
639 assert!(config.contains("backend = \"llvm\""));
640 assert!(config.contains("canonicalize, dce, tensor-fusion"));
641 assert!(config.contains("max_flops = 1000000000"));
642 }
643}