use std::time::Instant;
use burn::module::Module;
use burn::record::{FileRecorder, RecorderError};
use burn::tensor::cast::ToElement;
use burn::{
config::Config,
nn::{RotaryEncoding, RotaryEncodingConfig},
tensor::{
Device, ElementConversion, Int, Shape, Tensor, TensorData, activation::softmax,
backend::Backend,
},
};
#[cfg(feature = "import")]
use burn_store::{
KeyRemapper, ModuleSnapshot, PyTorchToBurnAdapter, PytorchStore, SafetensorsStore,
};
use crate::{
sampling::Sampler,
tokenizer::Tokenizer,
transformer::{KeyValueCache, Transformer, TransformerConfig},
};
#[cfg(feature = "pretrained")]
#[allow(unused_imports)]
use crate::pretrained::{self, ModelMeta};
#[cfg(feature = "tiny")]
use crate::tokenizer::SentiencePieceTokenizer;
#[cfg(feature = "llama3")]
use crate::tokenizer::Tiktoken;
#[derive(Config, Debug)]
pub struct LlamaConfig {
#[config(default = "4096")]
pub d_model: usize,
pub hidden_size: usize,
#[config(default = "32")]
pub num_hidden_layers: usize,
#[config(default = "32")]
pub num_attention_heads: usize,
pub num_key_value_heads: Option<usize>,
pub vocab_size: usize,
#[config(default = "1e-5")]
pub norm_eps: f64,
#[config(default = "RopeConfig::new(10000.0)")]
pub rope: RopeConfig,
#[config(default = "128")]
pub max_seq_len: usize,
#[config(default = "1")]
pub max_batch_size: usize,
pub tokenizer: String,
}
#[derive(Config, Debug)]
pub struct RopeConfig {
pub theta: f32,
#[config(default = "None")]
pub scaled: Option<RopeFrequencyScaling>,
}
#[derive(Config, Debug)]
pub struct RopeFrequencyScaling {
#[config(default = "8.")]
pub scale_factor: f32,
#[config(default = "1.")]
pub low_freq_factor: f32,
#[config(default = "4.")]
pub high_freq_factor: f32,
#[config(default = "8192.")]
pub old_context_len: f32,
}
impl LlamaConfig {
pub fn llama3_2_3b(tokenizer_path: &str) -> Self {
Self::new(8192, 128256, tokenizer_path.to_string())
.with_d_model(3072)
.with_num_hidden_layers(28)
.with_num_attention_heads(24)
.with_num_key_value_heads(Some(8))
.with_rope(
RopeConfig::new(500000.0)
.with_scaled(Some(RopeFrequencyScaling::new().with_scale_factor(32.))),
)
}
pub fn llama3_2_1b(tokenizer_path: &str) -> Self {
Self::new(8192, 128256, tokenizer_path.to_string())
.with_d_model(2048)
.with_num_hidden_layers(16)
.with_num_key_value_heads(Some(8))
.with_rope(
RopeConfig::new(500000.0)
.with_scaled(Some(RopeFrequencyScaling::new().with_scale_factor(32.))),
)
}
pub fn llama3_1_8b(tokenizer_path: &str) -> Self {
Self::new(14336, 128256, tokenizer_path.to_string())
.with_num_key_value_heads(Some(8))
.with_rope(RopeConfig::new(500000.0).with_scaled(Some(RopeFrequencyScaling::new())))
}
pub fn llama3_8b(tokenizer_path: &str) -> Self {
Self::new(14336, 128256, tokenizer_path.to_string())
.with_num_key_value_heads(Some(8))
.with_rope(RopeConfig::new(500000.0))
}
pub fn tiny_llama(tokenizer_path: &str) -> Self {
Self::new(5632, 32000, tokenizer_path.to_string())
.with_d_model(2048)
.with_num_hidden_layers(22)
.with_num_key_value_heads(Some(4))
.with_rope(RopeConfig::new(10000.0))
}
#[cfg(feature = "llama3")]
#[cfg_attr(docsrs, doc(cfg(feature = "llama3")))]
pub fn load_llama3_2_3b<B: Backend>(
checkpoint: &str,
tokenizer_path: &str,
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
use burn::record::{HalfPrecisionSettings, NamedMpkFileRecorder};
let mut llama = Self::llama3_2_3b(tokenizer_path)
.with_max_seq_len(max_seq_len)
.init::<B, Tiktoken>(device)?;
let recorder = NamedMpkFileRecorder::<HalfPrecisionSettings>::new();
llama = llama
.load(checkpoint, &recorder)
.map_err(|err| format!("Failed to load pre-trained Llama model.\nError: {err}"))?;
Ok(llama)
}
#[cfg(all(feature = "llama3", feature = "pretrained"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "llama3", feature = "pretrained"))))]
pub fn llama3_2_3b_pretrained<B: Backend>(
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
check_context_length(max_seq_len, 128 * 1024);
let model = pretrained::Llama::Llama323bInstruct.pretrained();
let checkpoint = model
.download_weights()
.map_err(|err| format!("Could not download weights.\nError: {err}"))?;
let tokenizer = model
.download_tokenizer()
.map_err(|err| format!("Could not download tokenizer.\nError: {err}"))?;
Self::load_llama3_2_3b(
checkpoint.to_str().unwrap(),
tokenizer.to_str().unwrap(),
max_seq_len,
device,
)
}
#[cfg(feature = "llama3")]
#[cfg_attr(docsrs, doc(cfg(feature = "llama3")))]
pub fn load_llama3_2_1b<B: Backend>(
checkpoint: &str,
tokenizer_path: &str,
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
use burn::record::{HalfPrecisionSettings, NamedMpkFileRecorder};
let mut llama = Self::llama3_2_1b(tokenizer_path)
.with_max_seq_len(max_seq_len)
.init::<B, Tiktoken>(device)?;
let recorder = NamedMpkFileRecorder::<HalfPrecisionSettings>::new();
llama = llama
.load(checkpoint, &recorder)
.map_err(|err| format!("Failed to load pre-trained Llama model.\nError: {err}"))?;
Ok(llama)
}
#[cfg(all(feature = "llama3", feature = "pretrained"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "llama3", feature = "pretrained"))))]
pub fn llama3_2_1b_pretrained<B: Backend>(
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
check_context_length(max_seq_len, 128 * 1024);
let model = pretrained::Llama::Llama321bInstruct.pretrained();
let checkpoint = model
.download_weights()
.map_err(|err| format!("Could not download weights.\nError: {err}"))?;
let tokenizer = model
.download_tokenizer()
.map_err(|err| format!("Could not download tokenizer.\nError: {err}"))?;
Self::load_llama3_2_1b(
checkpoint.to_str().unwrap(),
tokenizer.to_str().unwrap(),
max_seq_len,
device,
)
}
#[cfg(feature = "llama3")]
#[cfg_attr(docsrs, doc(cfg(feature = "llama3")))]
pub fn load_llama3_1_8b<B: Backend>(
checkpoint: &str,
tokenizer_path: &str,
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
use burn::record::{HalfPrecisionSettings, NamedMpkFileRecorder};
let mut llama = Self::llama3_1_8b(tokenizer_path)
.with_max_seq_len(max_seq_len)
.init::<B, Tiktoken>(device)?;
let recorder = NamedMpkFileRecorder::<HalfPrecisionSettings>::new();
llama = llama
.load(checkpoint, &recorder)
.map_err(|err| format!("Failed to load pre-trained Llama model.\nError: {err}"))?;
Ok(llama)
}
#[cfg(all(feature = "llama3", feature = "pretrained"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "llama3", feature = "pretrained"))))]
pub fn llama3_1_8b_pretrained<B: Backend>(
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
check_context_length(max_seq_len, 128 * 1024);
let model = pretrained::Llama::Llama31Instruct.pretrained();
let checkpoint = model
.download_weights()
.map_err(|err| format!("Could not download weights.\nError: {err}"))?;
let tokenizer = model
.download_tokenizer()
.map_err(|err| format!("Could not download tokenizer.\nError: {err}"))?;
Self::load_llama3_1_8b(
checkpoint.to_str().unwrap(),
tokenizer.to_str().unwrap(),
max_seq_len,
device,
)
}
#[cfg(feature = "llama3")]
#[cfg_attr(docsrs, doc(cfg(feature = "llama3")))]
pub fn load_llama3_8b<B: Backend>(
checkpoint: &str,
tokenizer_path: &str,
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
use burn::record::{HalfPrecisionSettings, NamedMpkFileRecorder};
let mut llama = Self::llama3_8b(tokenizer_path)
.with_max_seq_len(max_seq_len)
.init::<B, Tiktoken>(device)?;
let recorder = NamedMpkFileRecorder::<HalfPrecisionSettings>::new();
llama = llama
.load(checkpoint, &recorder)
.map_err(|err| format!("Failed to load pre-trained Llama model.\nError: {err}"))?;
Ok(llama)
}
#[cfg(all(feature = "llama3", feature = "pretrained"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "llama3", feature = "pretrained"))))]
pub fn llama3_8b_pretrained<B: Backend>(
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, Tiktoken>, String> {
check_context_length(max_seq_len, 8 * 1024);
let model = pretrained::Llama::Llama3Instruct.pretrained();
let checkpoint = model
.download_weights()
.map_err(|err| format!("Could not download weights.\nError: {err}"))?;
let tokenizer = model
.download_tokenizer()
.map_err(|err| format!("Could not download tokenizer.\nError: {err}"))?;
Self::load_llama3_8b(
checkpoint.to_str().unwrap(),
tokenizer.to_str().unwrap(),
max_seq_len,
device,
)
}
#[cfg(feature = "tiny")]
#[cfg_attr(docsrs, doc(cfg(feature = "tiny")))]
pub fn load_tiny_llama<B: Backend>(
checkpoint: &str,
tokenizer_path: &str,
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, SentiencePieceTokenizer>, String> {
use burn::record::{HalfPrecisionSettings, NamedMpkFileRecorder};
let mut llama = Self::tiny_llama(tokenizer_path)
.with_max_seq_len(max_seq_len)
.init::<B, SentiencePieceTokenizer>(device)?;
let recorder = NamedMpkFileRecorder::<HalfPrecisionSettings>::new();
llama = llama
.load(checkpoint, &recorder)
.map_err(|err| format!("Failed to load pre-trained Llama model.\nError: {err}"))?;
Ok(llama)
}
#[cfg(all(feature = "tiny", feature = "pretrained"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "tiny", feature = "pretrained"))))]
pub fn tiny_llama_pretrained<B: Backend>(
max_seq_len: usize,
device: &Device<B>,
) -> Result<Llama<B, SentiencePieceTokenizer>, String> {
check_context_length(max_seq_len, 2 * 1024);
let model = pretrained::Llama::TinyLlama.pretrained();
let checkpoint = model
.download_weights()
.map_err(|err| format!("Could not download weights.\nError: {err}"))?;
let tokenizer = model
.download_tokenizer()
.map_err(|err| format!("Could not download tokenizer.\nError: {err}"))?;
Self::load_tiny_llama(
checkpoint.to_str().unwrap(),
tokenizer.to_str().unwrap(),
max_seq_len,
device,
)
}
pub fn init<B: Backend, T: Tokenizer>(
&self,
device: &Device<B>,
) -> Result<Llama<B, T>, String> {
let tokenizer = T::new(&self.tokenizer)?;
let num_key_value_heads = self.num_key_value_heads.unwrap_or(self.num_attention_heads);
let model = TransformerConfig::new(
self.vocab_size,
self.num_hidden_layers,
self.d_model,
self.hidden_size,
self.num_attention_heads,
num_key_value_heads,
)
.with_max_seq_len(self.max_seq_len)
.with_norm_eps(self.norm_eps)
.init(device);
let cache = (0..self.num_hidden_layers)
.map(|_| {
KeyValueCache::new(
self.max_batch_size,
num_key_value_heads,
self.max_seq_len,
self.d_model / self.num_attention_heads,
device,
)
})
.collect::<Vec<_>>();
let rope = RotaryEncodingConfig::new(
self.max_seq_len * 2,
self.d_model / self.num_attention_heads,
)
.with_theta(self.rope.theta);
let rope = if let Some(scaling) = &self.rope.scaled {
let freq_scaling_fn = move |x| scaling.freq_scaling_by_parts(x);
rope.init_with_frequency_scaling(freq_scaling_fn, device)
} else {
rope.init(device)
};
Ok(Llama {
tokenizer,
model,
cache,
rope,
device: device.clone(),
})
}
#[cfg(feature = "import")]
pub fn load_pretrained<B: Backend, T: Tokenizer>(
&self,
checkpoint: &str,
device: &Device<B>,
) -> Result<Llama<B, T>, String> {
let mut llama = self.init(device)?;
println!("Loading record...");
let now = Instant::now();
#[cfg(not(feature = "tiny"))]
let key_mappings: Vec<(&str, &str)> = vec![
(
"(layers\\.[0-9]+\\.feed_forward)\\.w1\\.(.+)",
"$1.swiglu.linear_inner.$2",
),
(
"(layers\\.[0-9]+\\.feed_forward)\\.w3\\.(.+)",
"$1.swiglu.linear_outer.$2",
),
("(.*)norm\\.weight", "${1}norm.gamma"),
];
#[cfg(feature = "tiny")]
let key_mappings: Vec<(&str, &str)> = vec![
("lm_head\\.(.+)", "output.$1"),
("model\\.(.+)", "$1"),
("embed_tokens\\.(.+)", "tok_embeddings.$1"),
(
"(layers\\.[0-9]+)\\.input_layernorm\\.(.+)",
"$1.attention_norm.$2",
),
(
"(layers\\.[0-9]+)\\.post_attention_layernorm\\.(.+)",
"$1.ffn_norm.$2",
),
(
"(layers\\.[0-9]+)\\.mlp\\.down_proj\\.(.+)",
"$1.feed_forward.w2.$2",
),
(
"(layers\\.[0-9]+)\\.mlp\\.gate_proj\\.(.+)",
"$1.feed_forward.swiglu.linear_inner.$2",
),
(
"(layers\\.[0-9]+)\\.mlp\\.up_proj\\.(.+)",
"$1.feed_forward.swiglu.linear_outer.$2",
),
(
"(layers\\.[0-9]+)\\.self_attn\\.k_proj\\.(.+)",
"$1.attention.wk.$2",
),
(
"(layers\\.[0-9]+)\\.self_attn\\.o_proj\\.(.+)",
"$1.attention.wo.$2",
),
(
"(layers\\.[0-9]+)\\.self_attn\\.q_proj\\.(.+)",
"$1.attention.wq.$2",
),
(
"(layers\\.[0-9]+)\\.self_attn\\.v_proj\\.(.+)",
"$1.attention.wv.$2",
),
("(.*)norm\\.weight", "${1}norm.gamma"),
];
let remapper = KeyRemapper::from_patterns(key_mappings).expect("Invalid key mapping regex");
if checkpoint.ends_with(".safetensors") {
let mut store = SafetensorsStore::from_file(checkpoint)
.with_from_adapter(PyTorchToBurnAdapter)
.remap(remapper);
llama
.model
.load_from(&mut store)
.map_err(|e| e.to_string())?;
} else {
let mut store = PytorchStore::from_file(checkpoint).remap(remapper);
llama
.model
.load_from(&mut store)
.map_err(|e| e.to_string())?;
}
let elapsed = now.elapsed().as_secs();
println!("Loaded in {}s", elapsed);
#[cfg(feature = "tiny")]
{
println!("Permuting TinyLlama attention weights...");
permute_rotary_weights(
&mut llama.model,
self.num_attention_heads,
self.num_key_value_heads.unwrap_or(self.num_attention_heads),
self.d_model,
device,
);
}
println!("Llama record loaded");
Ok(llama)
}
}
pub struct GenerationOutput {
pub text: String,
pub tokens: usize,
pub time: f64,
}
pub struct Llama<B: Backend, T: Tokenizer> {
pub tokenizer: T,
pub model: Transformer<B>,
pub cache: Vec<KeyValueCache<B>>,
pub rope: RotaryEncoding<B>,
pub device: Device<B>,
}
impl<B: Backend, T: Tokenizer> Llama<B, T> {
#[allow(clippy::single_range_in_vec_init)]
pub fn generate(
&mut self,
prompt: &str,
sample_len: usize,
temperature: f64,
sampler: &mut Sampler,
) -> GenerationOutput {
let input_tokens = self.tokenize(prompt);
let prompt_len = input_tokens.dims()[0];
let mut tokens = Tensor::<B, 1, Int>::empty([prompt_len + sample_len], &self.device);
tokens = tokens.slice_assign([0..prompt_len], input_tokens);
let stop_tokens = Tensor::from_ints(self.tokenizer.stop_ids().as_slice(), &self.device);
let mut num_tokens: usize = 0;
let mut input_pos = Tensor::<B, 1, Int>::arange(0..prompt_len as i64, &self.device);
let now = Instant::now();
for i in 0..sample_len {
let x = tokens.clone().select(0, input_pos.clone()).reshape([1, -1]);
let logits = self.model.forward(x, &mut self.cache, &self.rope);
let [batch_size, seq_len, _vocab_size] = logits.dims();
let mut next_token_logits = logits
.slice([0..batch_size, seq_len - 1..seq_len])
.squeeze_dim(1);
if temperature > 0.0 {
next_token_logits = temperature_scaled_softmax(next_token_logits, temperature);
};
let next_token = sampler.sample(next_token_logits).squeeze_dim(0);
if stop_tokens
.clone()
.equal(next_token.clone())
.any()
.into_scalar()
.to_bool()
{
break;
}
tokens = tokens.slice_assign([prompt_len + i..prompt_len + i + 1], next_token);
num_tokens += 1;
let t = input_pos.dims()[0];
input_pos = input_pos.slice([t - 1..t]) + 1;
}
let tokens = tokens.into_data().as_slice::<B::IntElem>().unwrap()
[prompt_len..prompt_len + num_tokens]
.iter()
.map(|t| t.elem::<u32>())
.collect::<Vec<_>>();
let generated = self.tokenizer.decode(tokens);
let elapsed = now.elapsed().as_secs_f64();
GenerationOutput {
text: generated,
tokens: num_tokens,
time: elapsed,
}
}
fn tokenize(&self, text: &str) -> Tensor<B, 1, Int> {
let bos = !cfg!(feature = "tiny"); let tokens = self.tokenizer.encode(text, bos, false);
let shape = Shape::new([tokens.len()]);
Tensor::<B, 1, Int>::from_data(TensorData::new(tokens, shape), &self.device)
}
pub fn save<R: FileRecorder<B>>(
self,
file_path: &str,
recorder: &R,
) -> Result<(), RecorderError> {
println!("Saving record...");
let now = Instant::now();
self.model.save_file(file_path, recorder)?;
let elapsed = now.elapsed().as_secs();
println!("Saved in {}s", elapsed);
Ok(())
}
pub fn load<R: FileRecorder<B>>(
mut self,
file_path: &str,
recorder: &R,
) -> Result<Self, RecorderError> {
println!("Loading record...");
let now = Instant::now();
self.model = self.model.load_file(file_path, recorder, &self.device)?;
let elapsed = now.elapsed().as_secs();
println!("Loaded in {}s", elapsed);
Ok(self)
}
pub fn reset(&mut self) {
self.cache.iter_mut().for_each(|cache| cache.reset());
}
}
impl RopeFrequencyScaling {
pub fn freq_scaling_by_parts<B: Backend>(&self, freqs: Tensor<B, 1>) -> Tensor<B, 1> {
let low_freq_wavelen = self.old_context_len / self.low_freq_factor;
let high_freq_wavelen = self.old_context_len / self.high_freq_factor;
let wavelen = freqs.clone().recip().mul_scalar(2. * core::f32::consts::PI);
let cond = wavelen.clone().greater_equal_elem(high_freq_wavelen);
let smooth = wavelen
.clone()
.recip()
.mul_scalar(self.old_context_len)
.sub_scalar(self.low_freq_factor)
.div_scalar(self.high_freq_factor - self.low_freq_factor);
let new_freqs = smooth
.clone()
.neg()
.add_scalar(1.)
.mul(freqs.clone().div_scalar(self.scale_factor))
.add(smooth.clone().mul(freqs.clone()));
let new_freqs = freqs.clone().mask_where(cond, new_freqs);
let cond = wavelen.clone().greater_elem(low_freq_wavelen);
let new_freqs = new_freqs.mask_where(cond, freqs.clone().div_scalar(self.scale_factor));
let cond = wavelen.lower_elem(high_freq_wavelen);
new_freqs.mask_where(cond, freqs)
}
}
#[cfg(feature = "pretrained")]
#[allow(dead_code)]
fn check_context_length(max_seq_len: usize, max_context_len: usize) {
if max_seq_len > max_context_len {
eprintln!(
"Warning: max_seq_len ({}) exceeds the model's maximum context length ({})",
max_seq_len, max_context_len
);
}
}
pub(crate) fn temperature_scaled_softmax<B: Backend>(
logits: Tensor<B, 2>,
temperature: f64,
) -> Tensor<B, 2> {
softmax(logits / temperature, 1)
}
#[cfg(all(feature = "tiny", feature = "import"))]
fn permute_rotary_weights<B: Backend>(
model: &mut Transformer<B>,
n_heads: usize,
n_kv_heads: usize,
d_model: usize,
device: &Device<B>,
) {
use burn_store::TensorSnapshot;
let snapshots = model.collect(None, None, false);
let modified: Vec<TensorSnapshot> = snapshots
.into_iter()
.map(|snapshot| {
let path = snapshot.full_path();
if path.contains(".wq.weight") {
permute_attention_weight::<B>(&snapshot, n_heads, device)
} else if path.contains(".wk.weight") {
let kv_dim = d_model * n_kv_heads / n_heads;
permute_attention_weight_with_dim::<B>(&snapshot, n_kv_heads, kv_dim, device)
} else {
snapshot
}
})
.collect();
model.apply(modified, None, None, false);
}
#[cfg(all(feature = "tiny", feature = "import"))]
fn permute_attention_weight<B: Backend>(
snapshot: &burn_store::TensorSnapshot,
n_heads: usize,
device: &Device<B>,
) -> burn_store::TensorSnapshot {
use burn::module::ParamId;
use burn_store::TensorSnapshot;
let data = snapshot.to_data().expect("Failed to get tensor data");
let [dim1, dim2] = [data.shape[0], data.shape[1]];
let tensor: Tensor<B, 2> = Tensor::from_data(data, device);
let permuted = tensor
.reshape([dim1, n_heads, 2, dim2 / n_heads / 2])
.swap_dims(2, 3)
.reshape([dim1, dim2]);
TensorSnapshot::from_data(
permuted.to_data(),
snapshot.path_stack.clone().unwrap_or_default(),
snapshot.container_stack.clone().unwrap_or_default(),
snapshot.tensor_id.unwrap_or_else(ParamId::new),
)
}
#[cfg(all(feature = "tiny", feature = "import"))]
fn permute_attention_weight_with_dim<B: Backend>(
snapshot: &burn_store::TensorSnapshot,
n_heads: usize,
out_dim: usize,
device: &Device<B>,
) -> burn_store::TensorSnapshot {
use burn::module::ParamId;
use burn_store::TensorSnapshot;
let data = snapshot.to_data().expect("Failed to get tensor data");
let dim1 = data.shape[0];
let tensor: Tensor<B, 2> = Tensor::from_data(data, device);
let permuted = tensor
.reshape([dim1, n_heads, 2, out_dim / n_heads / 2])
.swap_dims(2, 3)
.reshape([dim1, out_dim]);
TensorSnapshot::from_data(
permuted.to_data(),
snapshot.path_stack.clone().unwrap_or_default(),
snapshot.container_stack.clone().unwrap_or_default(),
snapshot.tensor_id.unwrap_or_else(ParamId::new),
)
}