use candle_core::{DType, IndexOp, Tensor};
use candle_mi::{HookSpec, MIModel, MITokenizer};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
const MODEL_ID: &str = "kuleshov-group/mdlm-owt";
const GEN_LEN: usize = 6;
const SEED: u64 = 0;
#[derive(Debug, Clone, Copy)]
enum Order {
Random,
Confidence,
Entropy,
}
impl Order {
const fn label(self) -> &'static str {
match self {
Self::Random => "random ",
Self::Confidence => "confidence",
Self::Entropy => "entropy ",
}
}
}
fn main() -> candle_mi::Result<()> {
let model = MIModel::from_pretrained(MODEL_ID)?;
let Ok(tokenizer) = MITokenizer::from_hf_cache("openai-community/gpt2")
.or_else(|_| MITokenizer::from_hf_cache("gpt2"))
else {
println!("GPT-2 tokenizer not found in the HuggingFace cache.");
println!(" hf-fm download-file openai-community/gpt2 tokenizer.json");
return Ok(());
};
let mask_id = u32::try_from(model.vocab_size() - 1).map_err(|e| {
candle_mi::MIError::Model(candle_core::Error::Msg(format!("mask id overflow: {e}")))
})?;
let prompt_text = "The weather today is";
let prompt = tokenizer.encode_raw(prompt_text)?;
let seq_len = prompt.len() + GEN_LEN;
println!(
"MDLM decoding-order analysis — prompt {prompt_text:?}, filling {GEN_LEN} positions\n"
);
println!(
"{:<10} | {:>11} | {:>11} | completion",
"order", "reveal conf", "pred stab"
);
println!("{}", "-".repeat(72));
for order in [Order::Random, Order::Confidence, Order::Entropy] {
let result = decode_with_order(&model, mask_id, &prompt, seq_len, order)?;
let filled = result.tokens.get(prompt.len()..).unwrap_or(&[]);
let text = tokenizer.decode(filled)?;
println!(
"{} | {:>11.3} | {:>11.3} | {:?}",
order.label(),
result.mean_reveal_confidence,
result.mean_prediction_stability,
text.trim()
);
}
println!(
"\nConfidence/entropy orders front-load confident, stable positions; random does not."
);
Ok(())
}
struct DecodeResult {
tokens: Vec<u32>,
mean_reveal_confidence: f32,
mean_prediction_stability: f32,
}
#[allow(clippy::too_many_lines)] fn decode_with_order(
model: &MIModel,
mask_id: u32,
prompt: &[u32],
seq_len: usize,
order: Order,
) -> candle_mi::Result<DecodeResult> {
#[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
let mask_idx = mask_id as usize;
let mut x = vec![mask_id; seq_len];
for (i, &token) in prompt.iter().take(seq_len).enumerate() {
if let Some(slot) = x.get_mut(i) {
*slot = token;
}
}
let mut rng = StdRng::seed_from_u64(SEED);
let mut conf_sum = 0.0_f32;
let mut reveals = 0_u32;
let mut stab_sum = 0.0_f32;
let mut stab_steps = 0_u32;
let mut prev_top1: Option<Vec<Option<u32>>> = None;
loop {
let masked: Vec<usize> = (0..seq_len)
.filter(|&i| x.get(i).copied() == Some(mask_id))
.collect();
if masked.is_empty() {
break;
}
let input = Tensor::new(x.as_slice(), model.device())?.unsqueeze(0)?;
let cache = model.forward(&input, &HookSpec::new())?;
let logits = cache
.output()
.i(0)?
.to_dtype(DType::F32)?
.to_vec2::<f32>()?;
let mut stats: Vec<(usize, u32, f32, f32, f32)> = Vec::with_capacity(masked.len());
for &pos in &masked {
if let Some(row) = logits.get(pos) {
let (token, p1, margin, entropy) = dist_stats(row, mask_idx);
stats.push((pos, token, p1, margin, entropy));
}
}
if let Some(prev) = &prev_top1 {
let mut same = 0_u32;
let mut total = 0_u32;
for &(pos, token, ..) in &stats {
if let Some(&Some(prev_token)) = prev.get(pos) {
total += 1;
if prev_token == token {
same += 1;
}
}
}
if total > 0 {
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
{
stab_sum += same as f32 / total as f32;
}
stab_steps += 1;
}
}
let mut cur_top1 = vec![None; seq_len];
for &(pos, token, ..) in &stats {
if let Some(slot) = cur_top1.get_mut(pos) {
*slot = Some(token);
}
}
prev_top1 = Some(cur_top1);
let chosen = match order {
Order::Random => {
let idx = rng.gen_range(0..stats.len());
stats.get(idx).map(|s| s.0)
}
Order::Confidence => stats
.iter()
.max_by(|a, b| a.3.partial_cmp(&b.3).unwrap_or(std::cmp::Ordering::Equal))
.map(|s| s.0),
Order::Entropy => stats
.iter()
.min_by(|a, b| a.4.partial_cmp(&b.4).unwrap_or(std::cmp::Ordering::Equal))
.map(|s| s.0),
};
let Some(chosen_pos) = chosen else { break };
if let Some(&(_, token, p1, ..)) = stats.iter().find(|s| s.0 == chosen_pos) {
conf_sum += p1;
reveals += 1;
if let Some(slot) = x.get_mut(chosen_pos) {
*slot = token;
}
}
}
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
let mean_reveal_confidence = if reveals > 0 {
conf_sum / reveals as f32
} else {
0.0
};
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
let mean_prediction_stability = if stab_steps > 0 {
stab_sum / stab_steps as f32
} else {
0.0
};
Ok(DecodeResult {
tokens: x,
mean_reveal_confidence,
mean_prediction_stability,
})
}
#[allow(clippy::suboptimal_flops)]
fn dist_stats(row: &[f32], mask_idx: usize) -> (u32, f32, f32, f32) {
let max = row
.iter()
.enumerate()
.filter(|(i, _)| *i != mask_idx)
.map(|(_, &l)| l)
.fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = row
.iter()
.enumerate()
.map(|(i, &l)| if i == mask_idx { 0.0 } else { (l - max).exp() })
.collect();
let sum: f32 = exps.iter().sum::<f32>().max(f32::MIN_POSITIVE);
let mut top1 = (0_usize, -1.0_f32);
let mut top2 = -1.0_f32;
let mut entropy = 0.0_f32;
for (i, &e) in exps.iter().enumerate() {
let p = e / sum;
if p > 0.0 {
entropy -= p * p.ln();
}
if p > top1.1 {
top2 = top1.1;
top1 = (i, p);
} else if p > top2 {
top2 = p;
}
}
let margin = top1.1 - top2.max(0.0);
#[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
(top1.0 as u32, top1.1, margin, entropy)
}