1pub mod aggregate;
20pub mod dataset;
21pub mod draw;
22pub mod provision;
23pub mod report;
24pub mod sensitive;
25
26use std::collections::BTreeMap;
27use std::future::Future;
28use std::path::PathBuf;
29use std::time::{Duration, Instant};
30
31use anyhow::{Context, Result, bail};
32use serde::Deserialize;
33use serde_json::json;
34
35use crate::benchmark::{Benchmark, BenchmarkDescriptor};
36use crate::benchmarks::one_line;
37use crate::http;
38use crate::metadata::PluginMetadata;
39use crate::params::{ParamKind, ParamSpec, ParamValue, ParamValues};
40use crate::plugin::{Plugin, PluginHandle};
41use crate::result::{BenchmarkResult, LogLine, RunStatus};
42
43use draw::DrawSpec;
44
45pub use report::{
46 MLPERF_FLOOR_CHECKPOINTS, MLPERF_FLOOR_NORMALIZED, MLPERF_FLOOR_OVERALL,
47 is_mlperf_submission_checkpoint,
48};
49
50mod descriptors;
51mod mini_descriptors;
52pub use descriptors::{
53 ECHOLP_METADATA, FULL_DESCRIPTOR, FULL_METADATA, SUBSET_DESCRIPTOR, SUBSET_ECHOLP_DESCRIPTOR,
54 SUBSET_METADATA,
55};
56pub use mini_descriptors::{
57 MINI_METADATA, MINI_NEMOTRON_NANO_DESCRIPTOR, MINI_NEMOTRON_SUPER_DESCRIPTOR,
58 SUBSET_MINI_DESCRIPTOR,
59};
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum Variant {
63 Subset,
64 SubsetEcholp,
65 Full,
66 SubsetMini,
68}
69
70#[derive(Debug, Deserialize)]
72struct Scores {
73 overall_accuracy: f64,
74 normalized_single_turn_score: f64,
75 category_scores: BTreeMap<String, f64>,
76 subset_scores: BTreeMap<String, f64>,
77 total_samples: usize,
78 unmatched_responses: usize,
79 #[serde(default)]
83 subset_totals: BTreeMap<String, (u64, u64)>,
84}
85
86enum Phase {
87 Provision,
88 Generate,
89 Score,
90 Done,
91}
92
93pub struct Bfcl {
94 variant: Variant,
95 shard: Option<dataset::Shard>,
98 handle: Option<PluginHandle>,
99 phase: Phase,
100 artifacts: Option<provision::Artifacts>,
101 samples: Vec<dataset::Sample>,
102 cursor: usize,
103 responses: Vec<serde_json::Value>,
104 responses_path: Option<PathBuf>,
105 scores: Option<Scores>,
106 spec: DrawSpec,
107 max_new_tokens: usize,
108 temperature: f64,
109 request_timeout: Duration,
110 started: Option<Instant>,
111 tool_call_samples: usize,
112 transport_errors: usize,
115 known_sensitive_seen: usize,
118 target_model: Option<String>,
122 baseline_mins: report::BaselineMins,
126}
127
128pub const INHERIT_SHARD: &str = "inherit";
132
133pub const MAX_NEW_TOKENS: usize = 1024;
136
137impl Bfcl {
138 fn expected_samples(&self) -> Option<usize> {
142 let whole = self.variant.expected_samples()?;
143 match self.shard {
144 None => Some(whole),
145 Some(sh) => {
146 let totals = draw::reference_subset_totals();
147 let plan = draw::plan(&self.variant.spec(), &totals);
148 Some(
149 plan.iter()
150 .map(|(_, take)| draw::shard_take(*take, sh.index, sh.count))
151 .sum(),
152 )
153 }
154 }
155 }
156
157 pub fn new(variant: Variant) -> Self {
158 Self::maybe_sharded(variant, None)
159 }
160
161 pub fn sharded(variant: Variant, index: usize, count: usize) -> Self {
164 Self::maybe_sharded(variant, Some(dataset::Shard { index, count }))
165 }
166
167 fn maybe_sharded(variant: Variant, shard: Option<dataset::Shard>) -> Self {
168 Self {
169 variant,
170 shard,
171 handle: None,
172 phase: Phase::Provision,
173 artifacts: None,
174 samples: Vec::new(),
175 cursor: 0,
176 responses: Vec::new(),
177 responses_path: None,
178 scores: None,
179 spec: variant.spec(),
180 max_new_tokens: MAX_NEW_TOKENS,
181 temperature: 0.0,
182 request_timeout: Duration::from_secs(600),
183 started: None,
184 tool_call_samples: 0,
185 transport_errors: 0,
186 known_sensitive_seen: 0,
187 target_model: None,
188 baseline_mins: report::BaselineMins::default(),
189 }
190 }
191
192 fn handle(&self) -> Result<&PluginHandle> {
193 self.handle.as_ref().context("benchmark was not loaded")
194 }
195
196 fn elapsed(&self) -> Duration {
197 self.started.map(|s| s.elapsed()).unwrap_or_default()
198 }
199}
200
201impl Plugin for Bfcl {
202 fn metadata(&self) -> &'static PluginMetadata {
203 self.variant.metadata()
204 }
205
206 fn load(&mut self, handle: PluginHandle) -> impl Future<Output = Result<()>> + Send {
207 self.started = Some(Instant::now());
208 self.target_model = Some(handle.target().model.clone());
209 self.handle = Some(handle.clone());
210 async move {
211 let artifacts = provision::ensure(handle.artifacts(), &handle).await?;
212 self.artifacts = Some(artifacts);
213 Ok(())
214 }
215 }
216}
217
218impl Benchmark for Bfcl {
219 fn descriptor(&self) -> &'static BenchmarkDescriptor {
220 self.variant.descriptor()
221 }
222
223 fn parameters(&self) -> Vec<ParamSpec> {
224 let v = self.variant;
225 let mut specs = vec![
226 ParamSpec::new(
227 "non_live_pct",
228 "non_live %",
229 "Percentage of each non_live subset to draw. 62 is the golden MLPerf draw.",
230 ParamKind::Float {
231 min: 0.01,
232 max: 100.0,
233 },
234 ParamValue::Float(v.default_pct("non_live")),
235 ),
236 ParamSpec::new(
237 "live_pct",
238 "live %",
239 "Percentage of each live subset to draw. 10 is the golden MLPerf draw.",
240 ParamKind::Float {
241 min: 0.01,
242 max: 100.0,
243 },
244 ParamValue::Float(v.default_pct("live")),
245 ),
246 ParamSpec::new(
247 "hallucination_pct",
248 "hallucination %",
249 "Percentage of each hallucination subset to draw. 10 is the golden MLPerf draw.",
250 ParamKind::Float {
251 min: 0.01,
252 max: 100.0,
253 },
254 ParamValue::Float(v.default_pct("hallucination")),
255 ),
256 ParamSpec::new(
257 "subset_floor",
258 "Subset floor",
259 "Subsets this small are taken whole, so tiny ones do not collapse to noise.",
260 ParamKind::Int {
261 min: 0,
262 max: 10_000,
263 },
264 ParamValue::Int(v.default_floor() as i64),
265 ),
266 ParamSpec::new(
267 "max_new_tokens",
268 "Max new tokens",
269 "Output budget per sample. The MLPerf config uses 1024.",
270 ParamKind::Int {
271 min: 16,
272 max: 32_768,
273 },
274 ParamValue::Int(MAX_NEW_TOKENS as i64),
275 ),
276 ParamSpec::new(
277 "temperature",
278 "Temperature",
279 "0 is greedy, which is what every recorded BFCL score was produced with.",
280 ParamKind::Float { min: 0.0, max: 2.0 },
281 ParamValue::Float(0.0),
282 ),
283 ParamSpec::new(
284 "request_timeout_s",
285 "Request timeout",
286 "Seconds before one sample is abandoned and scored as no tool call.",
287 ParamKind::Int { min: 10, max: 3600 },
288 ParamValue::Int(600),
289 ),
290 ParamSpec::new(
291 "shard",
292 "Shard",
293 "Run one Nth of the draw, as `index/count` with a 0-based index \
294 (`2/7`; the whole draw is `0/1`). This is how a certification \
295 splits the gate across boxes: `met bench certify` picks the count \
296 from its fleet and runs every index; `gate::group` recombines the \
297 records. `inherit` runs whatever the constructor selected โ the \
298 whole draw for this id.",
299 ParamKind::Text,
300 ParamValue::Text(INHERIT_SHARD.to_string()),
307 ),
308 ];
309 specs.extend(report::BaselineMins::specs());
310 specs
311 }
312
313 fn configure(&mut self, values: &ParamValues) -> Result<()> {
314 let specs = self.parameters();
315 values.validate_against(&specs)?;
316 let floor = values.usize("subset_floor")?;
317 self.spec = DrawSpec {
318 categories: draw::CATEGORIES.iter().map(|c| c.to_string()).collect(),
319 category_pct: [
320 ("non_live".to_string(), values.float("non_live_pct")?),
321 ("live".to_string(), values.float("live_pct")?),
322 (
323 "hallucination".to_string(),
324 values.float("hallucination_pct")?,
325 ),
326 ]
327 .into_iter()
328 .collect(),
329 subset_floor: (floor > 0).then_some(floor),
330 };
331 let shard = values.text("shard")?;
333 if shard.trim() != INHERIT_SHARD {
334 self.shard = Some(dataset::Shard::parse(shard.trim()).map_err(anyhow::Error::msg)?);
335 }
336 self.max_new_tokens = values.usize("max_new_tokens")?;
337 self.temperature = values.float("temperature")?;
338 self.request_timeout = Duration::from_secs(values.usize("request_timeout_s")? as u64);
339 self.baseline_mins = report::BaselineMins::from_values(values)?;
340 self.phase = Phase::Provision;
341 self.cursor = 0;
342 self.responses.clear();
343 self.samples.clear();
344 self.scores = None;
345 self.tool_call_samples = 0;
346 self.known_sensitive_seen = 0;
347 Ok(())
348 }
349
350 async fn next(&mut self) -> Result<BenchmarkResult> {
351 let handle = self.handle()?.clone();
352 handle.check_cancelled()?;
353 match self.phase {
354 Phase::Provision => {
355 http::probe(handle.target(), Duration::from_secs(10))
356 .await
357 .context("endpoint probe failed โ check the target URL and port")?;
358 let artifacts = self
359 .artifacts
360 .clone()
361 .context("artifacts were not provisioned")?;
362 self.samples = dataset::load_shard(&artifacts.dataset, &self.spec, self.shard)?;
363 self.phase = Phase::Generate;
364 let n = self.samples.len();
365 let mut frame = BenchmarkResult::running("draw", self.elapsed())
366 .with_progress(0, n as u64)
367 .log_line(LogLine::info(format!(
368 "drew {n} samples across {} subsets",
369 self.samples
370 .iter()
371 .map(|s| s.subset.as_str())
372 .collect::<std::collections::BTreeSet<_>>()
373 .len()
374 )));
375 if let Some(want) = self.expected_samples()
378 && n != want
379 {
380 let of = match self.shard {
381 None => String::new(),
382 Some(sh) => format!(" (shard {} of {})", sh.index, sh.count),
383 };
384 frame = frame.log_line(LogLine::warn(format!(
385 "n={n}, not the pinned {want}{of} โ this run is NOT comparable to \
386 this draw's baseline"
387 )));
388 }
389 Ok(frame)
390 }
391 Phase::Generate => {
392 let total = self.samples.len() as u64;
393 if self.cursor >= self.samples.len() {
394 self.phase = Phase::Score;
395 handle.status("scoring with bfcl-eval");
396 return Ok(BenchmarkResult::running("scoring", self.elapsed())
397 .with_progress(total, total)
398 .with_summary(self.summary())
399 .log_line(LogLine::info(format!(
400 "generated {} responses; running the AST checker",
401 self.responses.len()
402 ))));
403 }
404 let subset = self.samples[self.cursor].subset.clone();
405 self.generate_one().await?;
406 let done = self.cursor as u64;
407 handle.progress(done, total);
408 handle.status(format!("{subset} ยท {done}/{total}"));
409 Ok(BenchmarkResult::running(subset, self.elapsed())
410 .with_progress(done, total)
411 .with_summary(self.summary()))
412 }
413 Phase::Score => {
414 let scores = self.score().await?;
415 if scores.unmatched_responses > 0 {
416 handle.warn(format!(
417 "{} response(s) did not match a dataset sample",
418 scores.unmatched_responses
419 ));
420 }
421 self.scores = Some(scores);
422 self.phase = Phase::Done;
423 let total = self.samples.len() as u64;
424 let mut frame = BenchmarkResult {
425 status: RunStatus::Completed,
426 ..BenchmarkResult::running("done", self.elapsed())
427 }
428 .with_progress(total, total)
429 .with_summary(self.summary())
430 .with_metrics(self.metrics())
431 .with_verdict(self.verdict());
432 if let Some(t) = self.table() {
433 frame = frame.with_table(t);
434 }
435 let mut fp = draw::fingerprint(
438 &self.spec,
439 self.samples.iter().map(|s| s.sample_id.as_str()),
440 );
441 if let Some(sh) = &self.shard {
442 fp.push_str(&format!(";shard={}/{}", sh.index, sh.count));
443 }
444 frame.dataset_fingerprint = Some(fp);
445 Ok(frame)
446 }
447 Phase::Done => bail!("next() was called after the run finished"),
448 }
449 }
450}
451
452#[path = "variant.rs"]
453mod variant_impl;
454
455#[path = "exec.rs"]
456mod exec;
457
458#[cfg(test)]
459#[path = "bfcl_tests.rs"]
460mod tests;
461
462#[cfg(test)]
463#[path = "bfcl_shard_tests.rs"]
464mod shard_tests;
465
466#[cfg(test)]
467#[path = "aggregate_tests.rs"]
468mod aggregate_tests;