mod codegen;
mod trace;
use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
};
use knok_compile::{compile_graph_with_registry, compile_mlir_source};
use knok_core::{Input, TensorType, TypedGraph};
pub use knok_build_macros::{
compile_graphs, compile_graphs_with_options, compile_mlir_models,
compile_mlir_models_with_options, graph,
};
pub use trace::*;
pub type Result<T> = anyhow::Result<T>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
LlvmCpu,
#[cfg(any(target_os = "macos", doc))]
MetalSpirv,
#[cfg(feature = "vulkan")]
VulkanSpirv,
#[cfg(feature = "cuda")]
Cuda,
}
impl Backend {
pub const fn name(self) -> &'static str {
match self {
Self::LlvmCpu => "llvm-cpu",
#[cfg(any(target_os = "macos", doc))]
Self::MetalSpirv => "metal-spirv",
#[cfg(feature = "vulkan")]
Self::VulkanSpirv => "vulkan-spirv",
#[cfg(feature = "cuda")]
Self::Cuda => "cuda",
}
}
pub const fn default_driver(self) -> &'static str {
match self {
Self::LlvmCpu => "local-task",
#[cfg(any(target_os = "macos", doc))]
Self::MetalSpirv => "metal",
#[cfg(feature = "vulkan")]
Self::VulkanSpirv => "vulkan",
#[cfg(feature = "cuda")]
Self::Cuda => "cuda",
}
}
}
#[derive(Clone, Debug)]
pub struct BuildOptions {
output_file: String,
stub_artifacts: bool,
}
impl BuildOptions {
pub fn stub_artifacts_for_check() -> Self {
Self {
stub_artifacts: true,
..Self::default()
}
}
pub fn output_file(mut self, name: impl Into<String>) -> Self {
self.output_file = name.into();
self
}
}
impl Default for BuildOptions {
fn default() -> Self {
Self {
output_file: "knok_graphs.rs".into(),
stub_artifacts: false,
}
}
}
#[derive(Default)]
pub struct GraphRegistry {
graphs: Vec<RegisteredGraph>,
}
pub struct RegisteredGraph {
graph: TypedGraph,
backend: Backend,
}
#[derive(Clone, Debug)]
pub struct MlirModel {
pub name: String,
pub path: PathBuf,
pub function_name: String,
pub backend: Backend,
pub inputs: Vec<Input>,
pub outputs: Vec<TensorType>,
}
impl MlirModel {
pub fn new(
name: impl Into<String>,
path: impl Into<PathBuf>,
function_name: impl Into<String>,
backend: Backend,
inputs: Vec<Input>,
outputs: Vec<TensorType>,
) -> Self {
Self {
name: name.into(),
path: path.into(),
function_name: function_name.into(),
backend,
inputs,
outputs,
}
}
}
impl GraphRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn trace<F, O>(&mut self, name: &str, backend: Backend, build: F) -> Result<()>
where
F: FnOnce(&mut TraceContext) -> O,
O: TraceOutput,
{
let mut context = TraceContext::default();
let output = build(&mut context);
let graph = context.finish(name, backend.name(), output)?;
self.graphs.push(RegisteredGraph { graph, backend });
Ok(())
}
}
pub fn emit_registered_graphs(registry: GraphRegistry) -> Result<()> {
emit_registered_graphs_with_options(registry, BuildOptions::default())
}
pub fn emit_registered_graphs_with_options(
registry: GraphRegistry,
options: BuildOptions,
) -> Result<()> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
fs::create_dir_all(&out_dir)?;
if env::var_os("KNOK_CACHE_DIR").is_none() {
env::set_var("KNOK_CACHE_DIR", out_dir.join("knok-cache"));
}
println!("cargo:rerun-if-changed=build.rs");
let mut generated = String::new();
let graphs = registry.graphs;
let graph_registry = graphs
.iter()
.map(|registered| (registered.graph.name.clone(), registered.graph.clone()))
.collect::<BTreeMap<_, _>>();
for registered in &graphs {
let vmfb_name = graph_vmfb_name(®istered.graph.name);
let vmfb_path = out_dir.join(&vmfb_name);
let compile_flags;
if options.stub_artifacts {
fs::write(&vmfb_path, [0u8; 16])?;
compile_flags = Vec::new();
} else {
let compiled = compile_registered_graph(®istered.graph, &graph_registry)?;
fs::write(&vmfb_path, compiled.vmfb)?;
compile_flags = Vec::new();
}
generated.push_str(&codegen::graph_module(
®istered.graph,
registered.backend,
&vmfb_name,
&compile_flags,
)?);
generated.push('\n');
}
fs::write(out_dir.join(options.output_file), generated)?;
Ok(())
}
pub fn emit_mlir_models(models: Vec<MlirModel>) -> Result<()> {
emit_mlir_models_with_options(models, BuildOptions::default())
}
pub fn emit_mlir_models_with_options(models: Vec<MlirModel>, options: BuildOptions) -> Result<()> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
fs::create_dir_all(&out_dir)?;
if env::var_os("KNOK_CACHE_DIR").is_none() {
env::set_var("KNOK_CACHE_DIR", out_dir.join("knok-cache"));
}
println!("cargo:rerun-if-changed=build.rs");
let mut generated = String::new();
for model in &models {
let source_path = resolve_model_path(&model.path)?;
println!("cargo:rerun-if-changed={}", source_path.display());
let vmfb_name = mlir_model_vmfb_name(&model.name);
let vmfb_path = out_dir.join(&vmfb_name);
let compile_flags;
if options.stub_artifacts {
fs::write(&vmfb_path, [0u8; 16])?;
compile_flags = Vec::new();
} else {
let source = fs::read_to_string(&source_path)?;
let vmfb = compile_mlir_source(model.backend.name(), &source)?;
fs::write(&vmfb_path, vmfb)?;
compile_flags = Vec::new();
}
generated.push_str(&codegen::mlir_model_module(
model,
&vmfb_name,
&compile_flags,
)?);
generated.push('\n');
}
fs::write(out_dir.join(options.output_file), generated)?;
Ok(())
}
fn resolve_model_path(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path.to_path_buf());
}
Ok(PathBuf::from(env::var("CARGO_MANIFEST_DIR")?).join(path))
}
fn graph_vmfb_name(name: &str) -> String {
format!("{name}.vmfb")
}
fn mlir_model_vmfb_name(name: &str) -> String {
format!("mlir-model-{name}.vmfb")
}
fn compile_registered_graph(
graph: &TypedGraph,
graphs: &BTreeMap<String, TypedGraph>,
) -> Result<knok_compile::CompiledGraph> {
compile_graph_with_registry(graph, graphs).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn external_mlir_artifacts_do_not_collide_with_traced_graph_artifacts() {
assert_eq!(graph_vmfb_name("forward"), "forward.vmfb");
assert_eq!(mlir_model_vmfb_name("forward"), "mlir-model-forward.vmfb");
assert_ne!(graph_vmfb_name("forward"), mlir_model_vmfb_name("forward"));
}
#[test]
fn backend_names_match_iree_driver_pairs() {
assert_eq!(Backend::LlvmCpu.name(), "llvm-cpu");
assert_eq!(Backend::LlvmCpu.default_driver(), "local-task");
#[cfg(any(target_os = "macos", doc))]
{
assert_eq!(Backend::MetalSpirv.name(), "metal-spirv");
assert_eq!(Backend::MetalSpirv.default_driver(), "metal");
}
#[cfg(feature = "vulkan")]
{
assert_eq!(Backend::VulkanSpirv.name(), "vulkan-spirv");
assert_eq!(Backend::VulkanSpirv.default_driver(), "vulkan");
}
#[cfg(feature = "cuda")]
{
assert_eq!(Backend::Cuda.name(), "cuda");
assert_eq!(Backend::Cuda.default_driver(), "cuda");
}
}
}
pub mod prelude {
pub use crate::{
abs, all, all_axis, amax, amax_axis, amin, amin_axis, any, any_axis, arange, arange_step,
arange_to, argmax, argmax_axis, argmin, argmin_axis, broadcast, ceil, clip, compile_graphs,
compile_graphs_with_options, compile_mlir_models, compile_mlir_models_with_options, concat,
conv2d, conv2d_options, cos, diagonal, diagonal_axes, dot, equal, exp, exp2, expm1, eye,
flip, flip_axes, floor, full_like, gather, graph, greater, greater_equal, identity, inner,
isnan, less, less_equal, linspace, log, log10, log1p, log2, logical_and, logical_not,
logical_or, logical_xor, matmul, max, max_axis, maximum, mean, mean_axis, min, min_axis,
minimum, moveaxis, not_equal, ones_like, outer, pad, permute, permute_dims, pow, prod,
prod_axis, ptp, ptp_axis, r#where, reciprocal, relu, repeat, reshape, rint, roll, round,
sigmoid, sin, slice, softmax, softmax_axis, split, sqrt, square, squeeze, stack, std,
std_axis, sum, sum_axis, swapaxes, take, take_along_axis, tan, tanh, tile, trace,
trace_axes, transpose, transpose_axes, unsqueeze, var, var_axis, vecdot, vecdot_axis,
zeros_like, Backend, BuildOptions, Conv2dOptions, MlirModel, Tensor0, Tensor1, Tensor2,
Tensor3, Tensor4, Tensor5, Tensor6, T0, T1, T2, T3, T4, T5, T6,
};
pub use half::{bf16, f16};
}
#[doc(hidden)]
pub mod __private {
pub use knok_core::{ElementType, Input, TensorType};
}