use anyhow::Result;
use ordered_float::OrderedFloat;
use sapient_hub::model_info::ModelInfo;
use sapient_ir::{graph::Graph, op::OpType};
pub fn build(info: &ModelInfo) -> Result<Graph> {
let mut g = Graph::new(format!("bert_{}", info.model_type));
let input_ids = g.add_input("input_ids", None, None);
let position_ids = g.add_input("position_ids", None, None);
let token_type_ids = g.add_input("token_type_ids", None, None);
let _attn_mask = g.add_input("attention_mask", None, None);
let word_emb = g.add_op(
OpType::Embedding {
vocab_size: info.vocab_size,
dim: info.hidden_size,
},
vec![input_ids],
1,
Some("embeddings.word_embeddings".into()),
);
let pos_emb = g.add_op(
OpType::Embedding {
vocab_size: info.max_position_embeddings,
dim: info.hidden_size,
},
vec![position_ids],
1,
Some("embeddings.position_embeddings".into()),
);
let tt_emb = g.add_op(
OpType::Embedding {
vocab_size: 2,
dim: info.hidden_size,
},
vec![token_type_ids],
1,
Some("embeddings.token_type_embeddings".into()),
);
let sum1 = g.add_op(
OpType::Add,
vec![word_emb, pos_emb],
1,
Some("embed_sum1".into()),
);
let sum2 = g.add_op(
OpType::Add,
vec![sum1, tt_emb],
1,
Some("embed_sum2".into()),
);
let eps = OrderedFloat(info.rms_norm_eps.max(1e-12));
let mut x = g.add_op(
OpType::LayerNorm {
axis: -1,
epsilon: eps,
},
vec![sum2],
1,
Some("embeddings.LayerNorm".into()),
);
for i in 0..info.num_hidden_layers {
let p = format!("encoder.layer.{i}");
let norm1 = g.add_op(
OpType::LayerNorm {
axis: -1,
epsilon: eps,
},
vec![x],
1,
Some(format!("{p}.attention.output.LayerNorm")),
);
let q = g.add_op(
OpType::MatMul,
vec![norm1],
1,
Some(format!("{p}.attention.self.query")),
);
let k = g.add_op(
OpType::MatMul,
vec![norm1],
1,
Some(format!("{p}.attention.self.key")),
);
let v = g.add_op(
OpType::MatMul,
vec![norm1],
1,
Some(format!("{p}.attention.self.value")),
);
let attn = g.add_op(
OpType::MultiHeadAttention {
num_heads: info.num_attention_heads,
head_dim: info.head_dim,
causal: false,
scale: None,
},
vec![q, k, v],
1,
Some(format!("{p}.attention.self")),
);
let out = g.add_op(
OpType::MatMul,
vec![attn],
1,
Some(format!("{p}.attention.output.dense")),
);
let x1 = g.add_op(OpType::Add, vec![x, out], 1, Some(format!("{p}.attn_res")));
let ff1 = g.add_op(
OpType::MatMul,
vec![x1],
1,
Some(format!("{p}.intermediate.dense")),
);
let act = g.add_op(
OpType::Gelu,
vec![ff1],
1,
Some(format!("{p}.intermediate.act")),
);
let ff2 = g.add_op(
OpType::MatMul,
vec![act],
1,
Some(format!("{p}.output.dense")),
);
let x2 = g.add_op(OpType::Add, vec![x1, ff2], 1, Some(format!("{p}.ffn_res")));
x = g.add_op(
OpType::LayerNorm {
axis: -1,
epsilon: eps,
},
vec![x2],
1,
Some(format!("{p}.output.LayerNorm")),
);
}
g.mark_output(x, "last_hidden_state");
let pooler = g.add_op(OpType::MatMul, vec![x], 1, Some("pooler.dense".into()));
let pooler_act = g.add_op(
OpType::Tanh,
vec![pooler],
1,
Some("pooler.activation".into()),
);
g.mark_output(pooler_act, "pooler_output");
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
const CFG: &str = r#"{"architectures":["BertForMaskedLM"],"model_type":"bert","vocab_size":30522,"hidden_size":64,"num_hidden_layers":2,"num_attention_heads":4,"intermediate_size":256,"max_position_embeddings":512,"rms_norm_eps":1e-12,"hidden_act":"gelu","rope_theta":10000.0}"#;
#[test]
fn tiny_bert_builds() {
let info = sapient_hub::model_info::ModelInfo::from_json_str(CFG).unwrap();
let g = build(&info).unwrap();
assert_eq!(g.outputs.len(), 2, "BERT should have 2 outputs");
}
}