metrale_bench/benchmarks/bfcl/
draw.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The BFCL sample draw. `normalized_single_turn_score` depends on
4//! the category mix, so a score is comparable only with scores on the same
5//! draw.
6//!
7//! Two deterministic rules, with no RNG, produce it:
8//!
9//! 1. Selection by category. Asking for `[non_live, live, hallucination]`
10//!    expands to those categories' subsets; `live_relevance` belongs to none of
11//!    them, so it drops out. That exclusion is the difference between n = 1011
12//!    and the golden n = 995.
13//! 2. Per-subset head(n). `n = total if total <= subset_floor else
14//!    max(1, int(total * pct / 100))`, taking the first `n` rows, concatenated
15//!    in sorted subset order.
16//!
17//! Owner: bench, BFCL benchmark.
18//! Invariants: `shard_take` over `0..count` sums to `take` for every `count`
19//! of at least 1.
20
21use std::collections::BTreeMap;
22
23/// 2026-09-26: Every single-turn subset, in the order `provision.py` writes
24/// them.
25pub const SINGLE_TURN_SUBSETS: [&str; 13] = [
26    "simple_python",
27    "simple_java",
28    "simple_javascript",
29    "multiple",
30    "parallel",
31    "parallel_multiple",
32    "live_simple",
33    "live_multiple",
34    "live_parallel",
35    "live_parallel_multiple",
36    "irrelevance",
37    "live_irrelevance",
38    "live_relevance",
39];
40
41/// 2026-09-26: The three scored categories.
42pub const CATEGORIES: [&str; 3] = ["non_live", "live", "hallucination"];
43
44/// 2026-09-26: Which category a subset belongs to. `live_relevance` maps to
45/// `None`: `score.py` scores it per sample but in no category, and a category
46/// selection leaves it out.
47pub fn category_of(subset: &str) -> Option<&'static str> {
48    match subset {
49        "simple_python" | "simple_java" | "simple_javascript" | "multiple" | "parallel"
50        | "parallel_multiple" => Some("non_live"),
51        "live_simple" | "live_multiple" | "live_parallel" | "live_parallel_multiple" => {
52            Some("live")
53        }
54        "irrelevance" | "live_irrelevance" => Some("hallucination"),
55        _ => None,
56    }
57}
58
59/// 2026-09-26: Sampling configuration.
60#[derive(Clone, Debug, PartialEq)]
61pub struct DrawSpec {
62    /// 2026-09-26: Categories to include. Empty means every single-turn
63    /// subset, including the uncategorised `live_relevance`.
64    pub categories: Vec<String>,
65    /// 2026-09-26: Per-category percentage. A selected subset whose category
66    /// has no entry is taken whole.
67    pub category_pct: BTreeMap<String, f64>,
68    /// 2026-09-26: A subset of at most this many rows is taken in full,
69    /// bypassing the percentage, so `live_parallel` (16 rows) is not cut to one
70    /// to three samples.
71    pub subset_floor: Option<usize>,
72}
73
74impl DrawSpec {
75    fn categories_or_all(categories: &[&str]) -> Vec<String> {
76        categories.iter().map(|s| s.to_string()).collect()
77    }
78
79    /// 2026-09-26: The golden MLPerf-edge draw: the three categories at
80    /// 62/10/10 with a floor of 25. On `reference_subset_totals` this is 995.
81    pub fn golden() -> Self {
82        Self {
83            categories: Self::categories_or_all(&CATEGORIES),
84            category_pct: [
85                ("non_live".to_string(), 62.0),
86                ("live".to_string(), 10.0),
87                ("hallucination".to_string(), 10.0),
88            ]
89            .into_iter()
90            .collect(),
91            subset_floor: Some(25),
92        }
93    }
94
95    /// 2026-09-26: Every sample of the three scored categories, no sampling.
96    /// The same categories as `golden`: taking `live_relevance` too would move
97    /// `overall_accuracy` on a subset no category scores.
98    pub fn full() -> Self {
99        Self {
100            categories: Self::categories_or_all(&CATEGORIES),
101            category_pct: BTreeMap::new(),
102            subset_floor: None,
103        }
104    }
105
106    /// 2026-09-26: The `echolp` draw: the three categories at 46/23/12 with a
107    /// floor of 25. On `reference_subset_totals` this is 1004. It weights `live`
108    /// more than twice as heavily as `golden` (23 % vs 10 %), so a score from one
109    /// draw is not comparable to a threshold from the other; each has its own
110    /// baseline.
111    pub fn echolp() -> Self {
112        Self {
113            categories: Self::categories_or_all(&CATEGORIES),
114            category_pct: [
115                ("non_live".to_string(), 46.0),
116                ("live".to_string(), 23.0),
117                ("hallucination".to_string(), 12.0),
118            ]
119            .into_iter()
120            .collect(),
121            subset_floor: Some(25),
122        }
123    }
124
125    /// 2026-10-03: The `mini` draw for a model's cheap correctness suite: the golden draw's
126    /// three categories at about a fifth of its rates (12.5/2/2) with a floor of 5, so the
127    /// category mix stays close to golden. On `reference_subset_totals` this is
128    /// [`MINI_SAMPLES`]. Not comparable to golden or echolp; it carries its own floors.
129    pub fn mini() -> Self {
130        Self {
131            categories: Self::categories_or_all(&CATEGORIES),
132            category_pct: [
133                ("non_live".to_string(), 12.5),
134                ("live".to_string(), 2.0),
135                ("hallucination".to_string(), 2.0),
136            ]
137            .into_iter()
138            .collect(),
139            subset_floor: Some(5),
140        }
141    }
142
143    /// 2026-09-26: Whether this subset is in the selection.
144    pub fn includes(&self, subset: &str) -> bool {
145        if self.categories.is_empty() {
146            return true;
147        }
148        category_of(subset).is_some_and(|c| self.categories.iter().any(|sel| sel == c))
149    }
150
151    /// 2026-09-26: How many rows to keep from a subset holding `total` rows.
152    pub fn take_count(&self, subset: &str, total: usize) -> usize {
153        if total == 0 || !self.includes(subset) {
154            return 0;
155        }
156        if self.subset_floor.is_some_and(|floor| total <= floor) {
157            return total;
158        }
159        match category_of(subset).and_then(|c| self.category_pct.get(c).copied()) {
160            // 2026-09-26: `as usize` truncates a non-negative f64, like the
161            // `int()` in the rule above; `max(1)` keeps a subset from vanishing.
162            Some(pct) => ((total as f64 * pct / 100.0) as usize).max(1),
163            None => total,
164        }
165    }
166}
167
168/// 2026-09-26: Applies the draw to per-subset totals, returning `(subset,
169/// take)` sorted by subset name and without subsets that take nothing.
170pub fn plan(spec: &DrawSpec, totals: &BTreeMap<String, usize>) -> Vec<(String, usize)> {
171    totals
172        .iter()
173        .map(|(subset, total)| (subset.clone(), spec.take_count(subset, *total)))
174        .filter(|(_, take)| *take > 0)
175        .collect()
176}
177
178/// 2026-10-03: The sample count of [`DrawSpec::mini`] on `reference_subset_totals`.
179pub const MINI_SAMPLES: usize = 192;
180
181/// 2026-10-03: The identity of the draw a run scored, for its record's `dataset_fingerprint`:
182/// `draw-sha256:<hex>;n=<N>;category_sample_pct=<cat>:<pct>,...;subset_floor=<f>`. The hash is
183/// over the run's sample ids in the order they were issued, one per line, so two runs share a
184/// fingerprint only when they scored the same samples in the same order; a BFCL score is not
185/// meaningful without it. A shard's fingerprint covers the shard's own slice.
186pub fn fingerprint<'a>(spec: &DrawSpec, sample_ids: impl IntoIterator<Item = &'a str>) -> String {
187    use sha2::{Digest, Sha256};
188    let mut hasher = Sha256::new();
189    let mut n = 0usize;
190    for id in sample_ids {
191        hasher.update(id.as_bytes());
192        hasher.update(b"\n");
193        n += 1;
194    }
195    let digest: String = hasher
196        .finalize()
197        .iter()
198        .map(|b| format!("{b:02x}"))
199        .collect();
200    let pct = spec
201        .category_pct
202        .iter()
203        .map(|(c, p)| format!("{c}:{p}"))
204        .collect::<Vec<_>>()
205        .join(",");
206    let floor = spec
207        .subset_floor
208        .map_or_else(|| "none".to_string(), |f| f.to_string());
209    format!("draw-sha256:{digest};n={n};category_sample_pct={pct};subset_floor={floor}")
210}
211
212/// 2026-09-26: Total sample count for a plan.
213pub fn total(plan: &[(String, usize)]) -> usize {
214    plan.iter().map(|(_, n)| n).sum()
215}
216
217/// 2026-09-26: The BFCL v4 single-turn row counts per subset, the table the
218/// draw and shard tests and `Bfcl::expected_samples` use. `provision.rs`
219/// saves the counts `provision.py` wrote as `dataset_summary.json`; they are
220/// expected to equal these.
221pub fn reference_subset_totals() -> BTreeMap<String, usize> {
222    [
223        ("irrelevance", 240),
224        ("live_irrelevance", 884),
225        ("live_multiple", 1053),
226        ("live_parallel", 16),
227        ("live_parallel_multiple", 24),
228        ("live_relevance", 16),
229        ("live_simple", 258),
230        ("multiple", 200),
231        ("parallel", 200),
232        ("parallel_multiple", 200),
233        ("simple_java", 100),
234        ("simple_javascript", 50),
235        ("simple_python", 400),
236    ]
237    .into_iter()
238    .map(|(k, v)| (k.to_string(), v))
239    .collect()
240}
241
242/// 2026-09-26: How many of a subset's `take` drawn rows shard `index` of
243/// `count` gets; 0 for an index out of range or a count of 0. Rows are striped,
244/// row `i` going to shard `i % count` (`shard_owns`), not cut into contiguous
245/// blocks, so every shard gets a proportional slice of every subset, and
246/// membership depends on position alone.
247pub fn shard_take(take: usize, index: usize, count: usize) -> usize {
248    if count == 0 || index >= count {
249        return 0;
250    }
251    // 2026-09-26: Rows index..take striding by `count` from `index`:
252    // ceil((take - index) / count).
253    take.saturating_sub(index).div_ceil(count)
254}
255
256/// 2026-09-26: Whether row `row` of a subset (0-based, within the drawn rows)
257/// is in shard `index` of `count`.
258pub fn shard_owns(row: usize, index: usize, count: usize) -> bool {
259    count > 0 && index < count && row % count == index
260}
261
262/// 2026-09-26: The per-subset counts one shard runs, without empty subsets.
263pub fn shard_plan(plan: &[(String, usize)], index: usize, count: usize) -> Vec<(String, usize)> {
264    plan.iter()
265        .map(|(s, take)| (s.clone(), shard_take(*take, index, count)))
266        .filter(|(_, n)| *n > 0)
267        .collect()
268}
269
270#[cfg(test)]
271#[path = "draw_tests.rs"]
272mod tests;