use anyhow::Result;
use rlx_flow::{BertQkvStyle, BuiltModel, CompileProfile, ModelFlow};
use rlx_ir::{DType, Shape};
use std::borrow::Cow;
use rlx_core::config::BertConfig;
use rlx_core::flow_util::WeightMapSource;
use rlx_core::weight_map::WeightMap;
#[derive(Debug, Clone)]
struct BertKeys {
prefix: &'static str,
qkv_style: BertQkvStyle,
}
impl BertKeys {
const WORD_EMBEDDINGS: &'static str = "embeddings.word_embeddings.weight";
const POSITION_EMBEDDINGS: &'static str = "embeddings.position_embeddings.weight";
const TOKEN_TYPE_EMBEDDINGS: &'static str = "embeddings.token_type_embeddings.weight";
const LN_WEIGHT: &'static str = "embeddings.LayerNorm.weight";
const LN_BIAS: &'static str = "embeddings.LayerNorm.bias";
const QKV_PROBE: &'static str = "encoder.layer.0.attention.self.query.weight";
fn detect(weights: &WeightMap) -> Self {
let prefix = if weights.has(&format!("bert.{}", Self::WORD_EMBEDDINGS)) {
"bert."
} else {
""
};
let qkv_style = if weights.has(&format!("{prefix}{}", Self::QKV_PROBE)) {
BertQkvStyle::Bert
} else {
BertQkvStyle::Mpnet
};
Self { prefix, qkv_style }
}
fn k(&self, suffix: &'static str) -> Cow<'static, str> {
if self.prefix.is_empty() {
Cow::Borrowed(suffix)
} else {
Cow::Owned(format!("{}{suffix}", self.prefix))
}
}
fn word_embeddings(&self) -> Cow<'static, str> {
self.k(Self::WORD_EMBEDDINGS)
}
fn position_embeddings(&self) -> Cow<'static, str> {
self.k(Self::POSITION_EMBEDDINGS)
}
fn token_type_embeddings(&self) -> Cow<'static, str> {
self.k(Self::TOKEN_TYPE_EMBEDDINGS)
}
fn embeddings_ln_weight(&self) -> Cow<'static, str> {
self.k(Self::LN_WEIGHT)
}
fn embeddings_ln_bias(&self) -> Cow<'static, str> {
self.k(Self::LN_BIAS)
}
fn layer_prefix(&self) -> &'static str {
self.prefix.trim_end_matches('.')
}
}
#[derive(Debug, Clone)]
pub struct BertFlow<'a> {
cfg: &'a BertConfig,
batch: usize,
seq: usize,
profile: CompileProfile,
}
impl<'a> BertFlow<'a> {
pub fn new(cfg: &'a BertConfig, batch: usize, seq: usize) -> Self {
Self {
cfg,
batch,
seq,
profile: CompileProfile::encoder(),
}
}
pub fn with_profile(mut self, profile: CompileProfile) -> Self {
self.profile = profile;
self
}
pub fn build(self, weights: &mut WeightMap) -> Result<BuiltModel> {
let keys = BertKeys::detect(weights);
let f = DType::F32;
let eps = self.cfg.layer_norm_eps as f32;
let flow = ModelFlow::new("bert")
.with_profile(self.profile)
.input("input_ids", Shape::new(&[self.batch, self.seq], f))
.input("attention_mask", Shape::new(&[self.batch, self.seq], f))
.input("token_type_ids", Shape::new(&[self.batch, self.seq], f))
.input("position_ids", Shape::new(&[self.batch, self.seq], f))
.embed(keys.word_embeddings())
.gather_add("position_ids", keys.position_embeddings())
.gather_add("token_type_ids", keys.token_type_embeddings())
.layer_norm(keys.embeddings_ln_weight(), keys.embeddings_ln_bias(), eps)
.repeat_bert_layers(
self.cfg.num_hidden_layers,
keys.layer_prefix(),
keys.qkv_style,
self.cfg.hidden_size,
self.cfg.num_attention_heads,
eps,
)
.output("hidden_states");
flow.build(&mut WeightMapSource(weights))
}
}
pub fn build_bert_built(
cfg: &BertConfig,
weights: &mut WeightMap,
batch: usize,
seq: usize,
) -> Result<BuiltModel> {
BertFlow::new(cfg, batch, seq).build(weights)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{tiny_bert_weights, weights_with_keys};
use rlx_core::config::BertConfig;
#[test]
fn bert_keys_detects_prefix_and_qkv_style() {
let k = BertKeys::detect(&weights_with_keys(&[
"embeddings.word_embeddings.weight",
"encoder.layer.0.attention.self.query.weight",
]));
assert_eq!(k.layer_prefix(), "");
assert_eq!(
k.word_embeddings().as_ref(),
"embeddings.word_embeddings.weight"
);
assert!(matches!(k.qkv_style, BertQkvStyle::Bert));
let k2 = BertKeys::detect(&weights_with_keys(&[
"bert.embeddings.word_embeddings.weight",
]));
assert_eq!(k2.layer_prefix(), "bert");
assert!(k2.embeddings_ln_bias().starts_with("bert."));
assert!(matches!(k2.qkv_style, BertQkvStyle::Mpnet));
}
#[test]
fn bert_flow_builds() {
let cfg = BertConfig {
vocab_size: 32,
hidden_size: 16,
num_hidden_layers: 1,
num_attention_heads: 4,
intermediate_size: 32,
max_position_embeddings: 32,
type_vocab_size: 2,
layer_norm_eps: 1e-12,
hidden_act: "gelu".into(),
};
let mut wm = tiny_bert_weights(&cfg);
let built = BertFlow::new(&cfg, 1, 4).build(&mut wm).unwrap();
assert!(built.into_hir().unwrap().len() > 10);
}
}