1use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Path, PathBuf};
10
11use clap::{Args, ValueEnum};
12#[cfg(feature = "cuda")]
13use ferrum_models::{
14 VNextDeterminismExecutionMode, VNextDeterminismExecutionSpec, VNextDeterminismInitialState,
15 VNextDeterminismPhase, VNextDeterminismWorkspacePoison,
16};
17#[cfg(any(feature = "cuda", test))]
18use ferrum_models::{VNextDeterminismParticipantSpec, MAX_VNEXT_DETERMINISM_PARTICIPANTS};
19use ferrum_types::{FerrumError, Result};
20
21const PRIMARY_MODEL_KEYS: [&str; 3] = ["m1-qwen35-4b", "m2-qwen35-35b-a3b", "m3-qwen3-30b-a3b"];
22const M1_MODEL_KEYS: [&str; 1] = ["m1-qwen35-4b"];
23#[cfg(feature = "cuda")]
24const EXECUTIONS_PER_MODE: usize = 6;
25#[cfg(feature = "cuda")]
26const RELEASE_EXPECTED_CASES: usize = 72;
27#[cfg(feature = "cuda")]
28const M1_S2_FOCUSED_EXPECTED_CASES: usize = 20;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
31pub enum VNextDeterminismScope {
32 #[default]
33 #[value(name = "release-full")]
34 ReleaseFull,
35 #[value(name = "m1-s2-focused")]
36 M1S2Focused,
37}
38
39impl VNextDeterminismScope {
40 const fn as_str(self) -> &'static str {
41 match self {
42 Self::ReleaseFull => "release-full",
43 Self::M1S2Focused => "m1-s2-focused",
44 }
45 }
46
47 const fn model_keys(self) -> &'static [&'static str] {
48 match self {
49 Self::ReleaseFull => &PRIMARY_MODEL_KEYS,
50 Self::M1S2Focused => &M1_MODEL_KEYS,
51 }
52 }
53
54 #[cfg(feature = "cuda")]
55 const fn expected_case_count(self) -> usize {
56 match self {
57 Self::ReleaseFull => RELEASE_EXPECTED_CASES,
58 Self::M1S2Focused => M1_S2_FOCUSED_EXPECTED_CASES,
59 }
60 }
61}
62
63#[derive(Args, Clone, Debug)]
64pub struct VNextDeterminismCommand {
65 #[arg(long, value_name = "PATH")]
67 pub models_lock: PathBuf,
68
69 #[arg(long, value_name = "DIR")]
71 pub artifact_root: PathBuf,
72
73 #[arg(long, value_enum, default_value_t = VNextDeterminismScope::ReleaseFull)]
75 pub scope: VNextDeterminismScope,
76
77 #[arg(
79 long = "model",
80 value_name = "MODEL_KEY=DIR",
81 action = clap::ArgAction::Append
82 )]
83 pub models: Vec<String>,
84}
85
86#[derive(Clone, Debug, PartialEq, Eq)]
87struct ModelBinding {
88 key: String,
89 directory: PathBuf,
90}
91
92#[cfg(any(feature = "cuda", test))]
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94enum ShapePhase {
95 Prefill,
96 Decode,
97}
98
99#[cfg(any(feature = "cuda", test))]
100impl ShapePhase {
101 #[cfg(feature = "cuda")]
102 const fn as_str(self) -> &'static str {
103 match self {
104 Self::Prefill => "prefill",
105 Self::Decode => "decode",
106 }
107 }
108
109 #[cfg(feature = "cuda")]
110 const fn execution_phase(self) -> VNextDeterminismPhase {
111 match self {
112 Self::Prefill => VNextDeterminismPhase::Prefill,
113 Self::Decode => VNextDeterminismPhase::Decode,
114 }
115 }
116}
117
118#[cfg(any(feature = "cuda", test))]
119#[derive(Clone, Debug, PartialEq, Eq)]
120struct ParticipantFixture {
121 token_ids: Vec<u32>,
122 immediate_start: usize,
123}
124
125#[cfg(any(feature = "cuda", test))]
126impl ParticipantFixture {
127 fn immediate_end(&self) -> usize {
128 self.token_ids.len()
129 }
130
131 fn to_spec(&self) -> Result<VNextDeterminismParticipantSpec> {
132 VNextDeterminismParticipantSpec::new(
133 self.token_ids.clone(),
134 self.immediate_start..self.immediate_end(),
135 self.immediate_end().saturating_add(8),
136 )
137 }
138}
139
140#[cfg(any(feature = "cuda", test))]
141#[derive(Clone, Debug, PartialEq, Eq)]
142struct ShapeFixture {
143 phase: ShapePhase,
144 partition: &'static str,
145 participants: Vec<ParticipantFixture>,
146}
147
148#[cfg(any(feature = "cuda", test))]
149impl ShapeFixture {
150 #[cfg(feature = "cuda")]
151 fn execution_spec(
152 &self,
153 initial_state: VNextDeterminismInitialState,
154 workspace_poison: VNextDeterminismWorkspacePoison,
155 mode: VNextDeterminismExecutionMode,
156 ) -> Result<VNextDeterminismExecutionSpec> {
157 VNextDeterminismExecutionSpec::new(
158 self.phase.execution_phase(),
159 self.participants
160 .iter()
161 .map(ParticipantFixture::to_spec)
162 .collect::<Result<Vec<_>>>()?,
163 initial_state,
164 workspace_poison,
165 mode,
166 )
167 }
168}
169
170pub async fn execute(command: VNextDeterminismCommand) -> Result<()> {
171 let bindings = validate_command(&command)?;
172 #[cfg(feature = "cuda")]
173 {
174 return cuda::collect(command, bindings).await;
175 }
176 #[cfg(not(feature = "cuda"))]
177 {
178 let _ = bindings;
179 Err(FerrumError::unsupported(
180 "ferrum vnext-determinism requires a binary built with the cuda feature",
181 ))
182 }
183}
184
185fn validate_command(command: &VNextDeterminismCommand) -> Result<Vec<ModelBinding>> {
186 require_regular_file(&command.models_lock, "--models-lock")?;
187 if !command.artifact_root.is_dir() {
188 return Err(FerrumError::invalid_parameter(format!(
189 "--artifact-root is not a directory: {}",
190 command.artifact_root.display()
191 )));
192 }
193 require_regular_file(
194 &command.artifact_root.join("hardware-probe/probe.json"),
195 "hardware-probe/probe.json",
196 )?;
197 parse_model_bindings(&command.models, command.scope, true)
198}
199
200fn require_regular_file(path: &Path, label: &str) -> Result<()> {
201 let metadata = path.symlink_metadata().map_err(|error| {
202 FerrumError::invalid_parameter(format!(
203 "{label} is not a readable regular file at {}: {error}",
204 path.display()
205 ))
206 })?;
207 if metadata.file_type().is_symlink() || !metadata.is_file() {
208 return Err(FerrumError::invalid_parameter(format!(
209 "{label} must be a real regular file: {}",
210 path.display()
211 )));
212 }
213 Ok(())
214}
215
216fn parse_model_bindings(
217 values: &[String],
218 scope: VNextDeterminismScope,
219 require_directories: bool,
220) -> Result<Vec<ModelBinding>> {
221 let model_keys = scope.model_keys();
222 let expected = model_keys.iter().copied().collect::<BTreeSet<_>>();
223 let mut indexed = BTreeMap::new();
224 for value in values {
225 let (key, raw_directory) = value.split_once('=').ok_or_else(|| {
226 FerrumError::invalid_parameter(format!(
227 "--model must use MODEL_KEY=DIR form, got {value:?}"
228 ))
229 })?;
230 if key.is_empty() || raw_directory.is_empty() || !expected.contains(key) {
231 return Err(FerrumError::invalid_parameter(format!(
232 "--model has an unknown key or empty directory: {value:?}"
233 )));
234 }
235 let directory = PathBuf::from(raw_directory);
236 if require_directories && (!directory.is_absolute() || !directory.is_dir()) {
237 return Err(FerrumError::invalid_parameter(format!(
238 "--model {key} must name an existing absolute directory: {}",
239 directory.display()
240 )));
241 }
242 if indexed.insert(key.to_owned(), directory).is_some() {
243 return Err(FerrumError::invalid_parameter(format!(
244 "--model duplicates primary model key {key}"
245 )));
246 }
247 }
248 let actual = indexed.keys().map(String::as_str).collect::<BTreeSet<_>>();
249 if actual != expected {
250 let missing = expected.difference(&actual).copied().collect::<Vec<_>>();
251 return Err(FerrumError::invalid_parameter(format!(
252 "--model must bind exactly the {} model set for scope {}; missing {missing:?}",
253 model_keys.len(),
254 scope.as_str(),
255 )));
256 }
257 Ok(model_keys
258 .iter()
259 .map(|key| ModelBinding {
260 key: (*key).to_owned(),
261 directory: indexed
262 .remove(*key)
263 .expect("exact primary model set was checked"),
264 })
265 .collect())
266}
267
268#[cfg(any(feature = "cuda", test))]
269fn shape_fixtures(scope: VNextDeterminismScope) -> Vec<ShapeFixture> {
270 let prefill = |partition, token_count, immediate_start| ShapeFixture {
271 phase: ShapePhase::Prefill,
272 partition,
273 participants: vec![ParticipantFixture {
274 token_ids: deterministic_tokens(0, token_count),
275 immediate_start,
276 }],
277 };
278 let decode = |partition, participant_count| ShapeFixture {
279 phase: ShapePhase::Decode,
280 partition,
281 participants: (0..participant_count)
282 .map(|participant| ParticipantFixture {
283 token_ids: deterministic_tokens(participant, 9),
284 immediate_start: 8,
285 })
286 .collect(),
287 };
288 let mut fixtures = vec![
289 prefill("single_token", 1, 0),
290 prefill("multi_token", 4, 0),
291 prefill("chunk_boundary", 8, 4),
292 decode("c1", 1),
293 decode("multi_participant", 4),
294 ];
295 if scope == VNextDeterminismScope::ReleaseFull {
296 fixtures.push(decode("c32", MAX_VNEXT_DETERMINISM_PARTICIPANTS));
297 }
298 fixtures
299}
300
301#[cfg(any(feature = "cuda", test))]
302fn deterministic_tokens(participant: usize, count: usize) -> Vec<u32> {
303 let base = 100_u32.saturating_add(
304 u32::try_from(participant)
305 .unwrap_or(u32::MAX)
306 .saturating_mul(16),
307 );
308 (0..count)
309 .map(|offset| base.saturating_add(u32::try_from(offset).unwrap_or(u32::MAX)))
310 .collect()
311}
312
313#[cfg(feature = "cuda")]
314mod cuda {
315 use std::fs::{self, File, OpenOptions};
316 use std::io::{Read, Write};
317 use std::sync::Arc;
318
319 use ferrum_engine::vnext_determinism::{
320 create_cuda_vnext_determinism_collector, CudaVNextDeterminismCollector,
321 };
322 use ferrum_interfaces::vnext::{
323 CapabilityCatalog, ContractVersion, ExecutionDeterminismEvidenceDenominator,
324 ExecutionDeterminismProviderCoverage, ProviderReplayEquivalence, ResolvedModelPlan,
325 SubmissionWaveDeterminismArtifactExecution, SubmissionWaveDeterminismArtifactWitness,
326 VNextError,
327 };
328 use ferrum_models::vnext::{
329 open_registered_colocated_safetensors, resolve_registered_model_from_sources,
330 DefinedProductionModel,
331 };
332 use ferrum_types::{Device, EngineConfig, ModelId};
333 use serde::{Deserialize, Serialize};
334 use sha2::{Digest, Sha256};
335 use uuid::Uuid;
336
337 use super::*;
338
339 const ARTIFACT_TYPE: &str = "runtime_vnext_cuda_determinism_collector";
340
341 #[derive(Debug, Serialize)]
342 struct TokenShapeArtifact {
343 partition: String,
344 participant_count: usize,
345 immediate_tokens: Vec<usize>,
346 source_start_tokens: Vec<usize>,
347 source_end_tokens: Vec<usize>,
348 }
349
350 impl TokenShapeArtifact {
351 fn from_fixture(fixture: &ShapeFixture) -> Self {
352 Self {
353 partition: fixture.partition.to_owned(),
354 participant_count: fixture.participants.len(),
355 immediate_tokens: fixture
356 .participants
357 .iter()
358 .map(|participant| {
359 participant
360 .immediate_end()
361 .saturating_sub(participant.immediate_start)
362 })
363 .collect(),
364 source_start_tokens: fixture
365 .participants
366 .iter()
367 .map(|participant| participant.immediate_start)
368 .collect(),
369 source_end_tokens: fixture
370 .participants
371 .iter()
372 .map(ParticipantFixture::immediate_end)
373 .collect(),
374 }
375 }
376 }
377
378 #[derive(Debug, Serialize)]
379 struct InitializationArtifact {
380 input_sha256: String,
381 rng_sha256: String,
382 initial_state_kind: String,
383 initial_state_sha256: String,
384 workspace_poison: String,
385 }
386
387 #[derive(Clone, Debug, Serialize)]
388 struct CoverageTargetArtifact {
389 operation_id: String,
390 operation_version: ContractVersion,
391 operation_fingerprint: String,
392 provider_id: String,
393 provider_version: ContractVersion,
394 provider_implementation_fingerprint: String,
395 provider_execution_contract_fingerprint: String,
396 replay_equivalence: String,
397 witness_plan_fingerprint: String,
398 node_ids: Vec<String>,
399 }
400
401 #[derive(Debug, Serialize)]
402 struct ComparisonArtifact {
403 kind: String,
404 ordinal: usize,
405 left_execution_id: String,
406 right_execution_id: String,
407 relation: &'static str,
408 first_mismatch: Option<String>,
409 }
410
411 #[derive(Debug, Serialize)]
412 struct CaseArtifact {
413 schema_version: u32,
414 case_id: String,
415 denominator_fingerprint: String,
416 binary_sha256: String,
417 device_runtime_implementation_fingerprint: String,
418 device_fingerprint: String,
419 model_key: String,
420 resolved_plan_fingerprint: String,
421 plan_hash: String,
422 phase: String,
423 token_shape: TokenShapeArtifact,
424 dtype: String,
425 quantization: String,
426 initialization: InitializationArtifact,
427 coverage_targets: Vec<CoverageTargetArtifact>,
428 executions: Vec<SubmissionWaveDeterminismArtifactExecution>,
429 comparisons: Vec<ComparisonArtifact>,
430 first_mismatch: Option<String>,
431 }
432
433 struct PendingCase {
434 case_id: String,
435 model_key: String,
436 phase: String,
437 token_shape: TokenShapeArtifact,
438 dtype: String,
439 quantization: String,
440 initial_state_kind: String,
441 workspace_poison: String,
442 executions: Vec<SubmissionWaveDeterminismArtifactExecution>,
443 comparisons: Vec<ComparisonArtifact>,
444 }
445
446 #[derive(Debug, Serialize)]
447 struct CaseProgressArtifact<'a> {
448 schema_version: u32,
449 artifact_type: &'static str,
450 status: &'static str,
451 case_id: &'a str,
452 model_key: &'a str,
453 phase: &'a str,
454 token_shape: &'a TokenShapeArtifact,
455 dtype: &'a str,
456 quantization: &'a str,
457 initial_state_kind: &'a str,
458 workspace_poison: &'a str,
459 initialization_identity:
460 &'a ferrum_interfaces::vnext::SubmissionWaveDeterminismArtifactInitializationIdentity,
461 execution_count: usize,
462 comparison_count: usize,
463 canonical_witness_count: usize,
464 canonical_witnesses_sha256: String,
465 replayed_segment_count: usize,
466 }
467
468 struct CollectedModel {
469 key: String,
470 directory: PathBuf,
471 plan: ResolvedModelPlan,
472 dtype: String,
473 quantization: String,
474 cases: Vec<PendingCase>,
475 }
476
477 #[derive(Debug, Deserialize)]
478 struct HardwareProbeIdentity {
479 schema_version: u32,
480 fingerprint: String,
481 }
482
483 #[derive(Debug, Serialize)]
484 struct FileReference {
485 path: String,
486 sha256: String,
487 size_bytes: u64,
488 }
489
490 #[derive(Debug, Serialize)]
491 struct DenominatorReference {
492 path: String,
493 sha256: String,
494 size_bytes: u64,
495 fingerprint: String,
496 }
497
498 #[derive(Debug, Serialize)]
499 struct CollectorModelSummary {
500 model_key: String,
501 model_dir: String,
502 resolved_plan_fingerprint: String,
503 plan_hash: String,
504 dtype: String,
505 quantization: String,
506 case_count: usize,
507 }
508
509 #[derive(Debug, Serialize)]
510 struct CollectorManifest {
511 schema_version: u32,
512 artifact_type: &'static str,
513 status: &'static str,
514 backend: &'static str,
515 scope: &'static str,
516 models_lock: FileReference,
517 hardware_probe: FileReference,
518 device_fingerprint: String,
519 binary: FileReference,
520 denominator: DenominatorReference,
521 models: Vec<CollectorModelSummary>,
522 cases: Vec<FileReference>,
523 case_count: usize,
524 execution_count: usize,
525 comparison_count: usize,
526 pass_line: String,
527 }
528
529 #[derive(Debug, Serialize)]
530 struct RejectionArtifact {
531 schema_version: u32,
532 artifact_type: &'static str,
533 status: &'static str,
534 failure_class: &'static str,
535 message: String,
536 }
537
538 pub(super) async fn collect(
539 command: VNextDeterminismCommand,
540 bindings: Vec<ModelBinding>,
541 ) -> Result<()> {
542 match collect_inner(&command, &bindings).await {
543 Ok(pass_line) => {
544 println!("{pass_line}");
545 Ok(())
546 }
547 Err(error) => {
548 let rejection = RejectionArtifact {
549 schema_version: 1,
550 artifact_type: ARTIFACT_TYPE,
551 status: "reject",
552 failure_class: "collector_failure",
553 message: error.to_string(),
554 };
555 let rejection_path = command.artifact_root.join("collector.reject.json");
556 let _ = write_json_exclusive(&rejection_path, &rejection);
557 Err(error)
558 }
559 }
560 }
561
562 async fn collect_inner(
563 command: &VNextDeterminismCommand,
564 bindings: &[ModelBinding],
565 ) -> Result<String> {
566 let fixtures = shape_fixtures(command.scope);
567 let expected_case_count = command.scope.expected_case_count();
568 let cases_per_model = fixtures.len() * 4;
569 reject_existing_outputs(&command.artifact_root)?;
570 let models_lock = file_reference(
571 &command.artifact_root,
572 &command.models_lock,
573 "models.lock.json",
574 )?;
575 let probe_path = command.artifact_root.join("hardware-probe/probe.json");
576 let hardware_probe = read_hardware_probe(&probe_path)?;
577 let hardware_probe_ref = file_reference(
578 &command.artifact_root,
579 &probe_path,
580 "hardware-probe/probe.json",
581 )?;
582 let current_exe = std::env::current_exe().map_err(|error| {
583 FerrumError::io(format!("cannot resolve current ferrum binary: {error}"))
584 })?;
585 let binary = absolute_file_reference(¤t_exe)?;
586 let progress_root = command.artifact_root.join("collector-progress");
587 let progress_cases = progress_root.join("cases");
588 fs::create_dir(&progress_root).map_err(|error| {
589 FerrumError::io(format!(
590 "cannot create determinism progress directory {}: {error}",
591 progress_root.display()
592 ))
593 })?;
594 fs::create_dir(&progress_cases).map_err(|error| {
595 FerrumError::io(format!(
596 "cannot create determinism progress case directory {}: {error}",
597 progress_cases.display()
598 ))
599 })?;
600
601 let mut canonical_catalog: Option<CapabilityCatalog> = None;
602 let mut canonical_catalog_fingerprint: Option<String> = None;
603 let mut collected_models = Vec::with_capacity(bindings.len());
604 for (model_ordinal, binding) in bindings.iter().enumerate() {
605 println!(
606 "FERRUM VNEXT DETERMINISM PROGRESS model={} stage=load ordinal={}/{}",
607 binding.key,
608 model_ordinal + 1,
609 bindings.len()
610 );
611 let sources = Arc::new(
612 open_registered_colocated_safetensors(&binding.directory).map_err(|error| {
613 FerrumError::model(format!(
614 "cannot open registered source for {} at {}: {error}",
615 binding.key,
616 binding.directory.display()
617 ))
618 })?,
619 );
620 let registration = resolve_registered_model_from_sources(sources.as_ref())
621 .and_then(|registration| registration.into_required())
622 .map_err(|error| {
623 FerrumError::model(format!(
624 "cannot require vNext registration for {}: {error}",
625 binding.key
626 ))
627 })?;
628 let prepared = registration.define_from_sources(sources).map_err(|error| {
629 FerrumError::model(format!(
630 "cannot prepare registered vNext model {}: {error}",
631 binding.key
632 ))
633 })?;
634 let capabilities = prepared.model_capabilities(
635 &ferrum_types::NumericalExecutionPolicy::Auto,
636 ferrum_types::KvStorageFormat::F16,
637 )?;
638 let quantization = capabilities
639 .quantization
640 .unwrap_or_else(|| "none".to_owned());
641 let mut engine = determinism_engine_config(binding, &prepared);
642 engine.backend.dtype = prepared.descriptor().execution_dtype();
643 let collector = create_cuda_vnext_determinism_collector(&engine, &prepared, 0)
644 .map_err(|error| {
645 FerrumError::backend(format!(
646 "cannot create CUDA determinism collector for {}: {error}",
647 binding.key
648 ))
649 })?;
650 let catalog_fingerprint = collector
651 .capability_catalog()
652 .fingerprint()
653 .map_err(vnext_backend_error)?;
654 match canonical_catalog_fingerprint.as_deref() {
655 Some(expected) if expected != catalog_fingerprint => {
656 return Err(FerrumError::backend(format!(
657 "CUDA capability catalog drifted between primary models: expected {expected}, got {catalog_fingerprint} for {}",
658 binding.key
659 )));
660 }
661 None => {
662 canonical_catalog = Some(collector.capability_catalog().clone());
663 canonical_catalog_fingerprint = Some(catalog_fingerprint);
664 }
665 Some(_) => {}
666 }
667 let plan = collector.resolved_model_plan().clone();
668 let dtype = collector.model_info().dtype.to_string();
669 collector.prepare().await.map_err(|error| {
670 FerrumError::backend(format!(
671 "cannot prepare CUDA determinism collector for {}: {error}",
672 binding.key
673 ))
674 })?;
675 println!(
676 "FERRUM VNEXT DETERMINISM PROGRESS model={} stage=prepared",
677 binding.key
678 );
679 let cases = collect_model_cases(
680 &collector,
681 &binding.key,
682 &dtype,
683 &quantization,
684 &fixtures,
685 model_ordinal * cases_per_model,
686 expected_case_count,
687 &progress_cases,
688 )
689 .await?;
690 drop(collector);
691 drop(prepared);
692 println!(
693 "FERRUM VNEXT DETERMINISM PROGRESS model={} stage=released cases={}",
694 binding.key,
695 cases.len()
696 );
697 collected_models.push(CollectedModel {
698 key: binding.key.clone(),
699 directory: binding.directory.clone(),
700 plan,
701 dtype,
702 quantization,
703 cases,
704 });
705 }
706
707 let catalog = canonical_catalog.ok_or_else(|| {
708 FerrumError::internal("CUDA determinism collector produced no capability catalog")
709 })?;
710 let plan_refs = collected_models
711 .iter()
712 .map(|model| (model.key.as_str(), &model.plan))
713 .collect::<Vec<_>>();
714 let provider_coverage = match command.scope {
715 VNextDeterminismScope::ReleaseFull => {
716 ExecutionDeterminismProviderCoverage::AllCatalogProviders
717 }
718 VNextDeterminismScope::M1S2Focused => {
719 ExecutionDeterminismProviderCoverage::SelectedPlanProviders
720 }
721 };
722 let denominator =
723 ExecutionDeterminismEvidenceDenominator::from_catalog_and_resolved_plans_with_provider_coverage(
724 &catalog,
725 &plan_refs,
726 provider_coverage,
727 )
728 .map_err(vnext_backend_error)?;
729 let denominator_bytes = denominator.to_json().map_err(vnext_backend_error)?;
730 let denominator_fingerprint = denominator.fingerprint().map_err(vnext_backend_error)?;
731 let device_runtime_fingerprint = denominator
732 .coverage()
733 .device_runtime_implementation_fingerprint()
734 .to_owned();
735
736 let stage = command.artifact_root.join(format!(
737 ".vnext-determinism-stage-{}-{}",
738 std::process::id(),
739 Uuid::new_v4()
740 ));
741 fs::create_dir(&stage).map_err(|error| {
742 FerrumError::io(format!(
743 "cannot create determinism staging directory {}: {error}",
744 stage.display()
745 ))
746 })?;
747 let stage_cases = stage.join("cases");
748 fs::create_dir(&stage_cases).map_err(|error| {
749 FerrumError::io(format!(
750 "cannot create determinism case staging directory {}: {error}",
751 stage_cases.display()
752 ))
753 })?;
754
755 let staged = stage_collection(
756 &stage,
757 collected_models,
758 &denominator,
759 &denominator_bytes,
760 &denominator_fingerprint,
761 &device_runtime_fingerprint,
762 &hardware_probe.fingerprint,
763 command.scope,
764 expected_case_count,
765 models_lock,
766 hardware_probe_ref,
767 binary,
768 );
769 let manifest = match staged {
770 Ok(manifest) => manifest,
771 Err(error) => {
772 let _ = fs::remove_dir_all(&stage);
773 return Err(error);
774 }
775 };
776 publish_stage(&stage, &command.artifact_root)?;
777 let pass_line = manifest.pass_line.clone();
778 Ok(pass_line)
779 }
780
781 fn determinism_engine_config(
782 binding: &ModelBinding,
783 prepared: &DefinedProductionModel,
784 ) -> EngineConfig {
785 let mut engine = EngineConfig::default();
786 engine.model.model_id = ModelId::new(binding.key.clone());
787 engine.backend.device = Device::CUDA(0);
788 engine.backend.dtype = prepared.descriptor().execution_dtype();
789 engine.backend.enable_reusable_execution = true;
790 engine.scheduler.max_running_requests = MAX_VNEXT_DETERMINISM_PARTICIPANTS;
791 engine.batching.max_batch_size = MAX_VNEXT_DETERMINISM_PARTICIPANTS;
792 engine.batching.max_num_batched_tokens = MAX_VNEXT_DETERMINISM_PARTICIPANTS;
793 engine.runtime.model_path = Some(binding.directory.display().to_string());
794 engine
795 }
796
797 async fn collect_model_cases(
798 collector: &CudaVNextDeterminismCollector,
799 model_key: &str,
800 dtype: &str,
801 quantization: &str,
802 fixtures: &[ShapeFixture],
803 completed_before_model: usize,
804 expected_case_count: usize,
805 progress_cases: &Path,
806 ) -> Result<Vec<PendingCase>> {
807 let mut cases = Vec::with_capacity(fixtures.len() * 4);
808 for fixture in fixtures {
809 for (initial_state, initial_state_kind) in [
810 (VNextDeterminismInitialState::Zero, "zero"),
811 (VNextDeterminismInitialState::Nonzero, "nonzero"),
812 ] {
813 let zero = collect_case(
814 collector,
815 model_key,
816 &fixture,
817 dtype,
818 quantization,
819 initial_state,
820 initial_state_kind,
821 VNextDeterminismWorkspacePoison::Zero,
822 "00",
823 )
824 .await?;
825 write_case_progress(progress_cases, &zero)?;
826 print_case_progress(
827 completed_before_model + cases.len() + 1,
828 expected_case_count,
829 &zero,
830 );
831 let a5 = collect_case(
832 collector,
833 model_key,
834 &fixture,
835 dtype,
836 quantization,
837 initial_state,
838 initial_state_kind,
839 VNextDeterminismWorkspacePoison::A5,
840 "a5",
841 )
842 .await?;
843 write_case_progress(progress_cases, &a5)?;
844 print_case_progress(
845 completed_before_model + cases.len() + 2,
846 expected_case_count,
847 &a5,
848 );
849 ensure_poison_equivalence(&zero, &a5)?;
850 cases.extend([zero, a5]);
851 }
852 }
853 Ok(cases)
854 }
855
856 fn write_case_progress(progress_cases: &Path, case: &PendingCase) -> Result<()> {
857 let first = case.executions.first().ok_or_else(|| {
858 FerrumError::internal(format!(
859 "determinism case {} contains no execution",
860 case.case_id
861 ))
862 })?;
863 let witness_bytes = serde_json::to_vec(first.witnesses())
864 .map_err(|error| FerrumError::serialization(error.to_string()))?;
865 let progress = CaseProgressArtifact {
866 schema_version: 1,
867 artifact_type: "runtime_vnext_cuda_determinism_case_progress",
868 status: "case_comparisons_pass",
869 case_id: &case.case_id,
870 model_key: &case.model_key,
871 phase: &case.phase,
872 token_shape: &case.token_shape,
873 dtype: &case.dtype,
874 quantization: &case.quantization,
875 initial_state_kind: &case.initial_state_kind,
876 workspace_poison: &case.workspace_poison,
877 initialization_identity: first.initialization_identity(),
878 execution_count: case.executions.len(),
879 comparison_count: case.comparisons.len(),
880 canonical_witness_count: first.witnesses().len(),
881 canonical_witnesses_sha256: format!("{:x}", Sha256::digest(&witness_bytes)),
882 replayed_segment_count: case
883 .executions
884 .iter()
885 .find(|execution| execution.mode() == "replay")
886 .map(|execution| execution.replayed_segments().len())
887 .unwrap_or(0),
888 };
889 write_json_exclusive(
890 &progress_cases.join(format!("{}.json", case.case_id)),
891 &progress,
892 )
893 }
894
895 fn print_case_progress(completed: usize, expected_case_count: usize, case: &PendingCase) {
896 println!(
897 "FERRUM VNEXT DETERMINISM PROGRESS case={} complete={}/{}",
898 case.case_id, completed, expected_case_count
899 );
900 }
901
902 async fn collect_case(
903 collector: &CudaVNextDeterminismCollector,
904 model_key: &str,
905 fixture: &ShapeFixture,
906 dtype: &str,
907 quantization: &str,
908 initial_state: VNextDeterminismInitialState,
909 initial_state_kind: &str,
910 workspace_poison: VNextDeterminismWorkspacePoison,
911 workspace_poison_label: &str,
912 ) -> Result<PendingCase> {
913 let case_id = format!(
914 "{model_key}.{}.{}.{}.{}",
915 fixture.phase.as_str(),
916 fixture.partition,
917 initial_state_kind,
918 workspace_poison_label
919 );
920 let mut executions = Vec::with_capacity(EXECUTIONS_PER_MODE * 2);
921 for (mode, mode_label) in [
922 (VNextDeterminismExecutionMode::Eager, "eager"),
923 (VNextDeterminismExecutionMode::Replayed, "replay"),
924 ] {
925 for repeat in 0..EXECUTIONS_PER_MODE {
926 let spec = fixture.execution_spec(initial_state, workspace_poison, mode)?;
927 let execution_id = format!("{mode_label}-{repeat:02}");
928 let evidence = collector.collect_execution(&spec).await.map_err(|error| {
929 FerrumError::backend(format!(
930 "determinism execution failed for case {case_id} execution {execution_id}: {error}"
931 ))
932 })?;
933 executions.push(
934 evidence
935 .into_artifact_execution(execution_id.clone())
936 .map_err(|error| {
937 FerrumError::backend(format!(
938 "determinism artifact projection failed for case {case_id} execution {execution_id}: {error}"
939 ))
940 })?,
941 );
942 }
943 }
944 executions.sort_by(|left, right| left.execution_id().cmp(right.execution_id()));
945 let comparisons = compare_case_executions(&executions)?;
946 Ok(PendingCase {
947 case_id,
948 model_key: model_key.to_owned(),
949 phase: fixture.phase.as_str().to_owned(),
950 token_shape: TokenShapeArtifact::from_fixture(fixture),
951 dtype: dtype.to_owned(),
952 quantization: quantization.to_owned(),
953 initial_state_kind: initial_state_kind.to_owned(),
954 workspace_poison: workspace_poison_label.to_owned(),
955 executions,
956 comparisons,
957 })
958 }
959
960 fn compare_case_executions(
961 executions: &[SubmissionWaveDeterminismArtifactExecution],
962 ) -> Result<Vec<ComparisonArtifact>> {
963 let by_id = executions
964 .iter()
965 .map(|execution| (execution.execution_id(), execution))
966 .collect::<BTreeMap<_, _>>();
967 let mut comparisons = Vec::with_capacity(15);
968 for (kind, left_mode, right_mode) in [
969 ("eager_eager", "eager", "eager"),
970 ("eager_replay", "eager", "replay"),
971 ("replay_replay", "replay", "replay"),
972 ] {
973 for ordinal in 0..5 {
974 let left_id = format!("{left_mode}-{ordinal:02}");
975 let right_ordinal = if kind == "eager_replay" {
976 ordinal
977 } else {
978 ordinal + 1
979 };
980 let right_id = format!("{right_mode}-{right_ordinal:02}");
981 let left = by_id.get(left_id.as_str()).ok_or_else(|| {
982 FerrumError::internal(format!(
983 "determinism comparison lacks execution {left_id}"
984 ))
985 })?;
986 let right = by_id.get(right_id.as_str()).ok_or_else(|| {
987 FerrumError::internal(format!(
988 "determinism comparison lacks execution {right_id}"
989 ))
990 })?;
991 ensure_execution_equivalence(left, right, kind)?;
992 if kind == "replay_replay"
993 && (left.compute_path_requirement() != right.compute_path_requirement()
994 || left.reusable_program_fingerprint()
995 != right.reusable_program_fingerprint()
996 || left.declared_eager_boundary_node_ids()
997 != right.declared_eager_boundary_node_ids()
998 || left.replayed_segments() != right.replayed_segments())
999 {
1000 return Err(FerrumError::backend(format!(
1001 "{kind} replay shape mismatch between {left_id} and {right_id}"
1002 )));
1003 }
1004 comparisons.push(ComparisonArtifact {
1005 kind: kind.to_owned(),
1006 ordinal,
1007 left_execution_id: left_id,
1008 right_execution_id: right_id,
1009 relation: "bitwise_equal",
1010 first_mismatch: None,
1011 });
1012 }
1013 }
1014 Ok(comparisons)
1015 }
1016
1017 fn ensure_execution_equivalence(
1018 left: &SubmissionWaveDeterminismArtifactExecution,
1019 right: &SubmissionWaveDeterminismArtifactExecution,
1020 comparison_kind: &str,
1021 ) -> Result<()> {
1022 let left_initialization = left.initialization_identity();
1023 let right_initialization = right.initialization_identity();
1024 let mut restore_mismatches = Vec::with_capacity(4);
1025 if left.restore_sha256() != right.restore_sha256() {
1026 restore_mismatches.push("logical_restore");
1027 }
1028 if left_initialization.input_sha256() != right_initialization.input_sha256() {
1029 restore_mismatches.push("external_input");
1030 }
1031 if left_initialization.rng_sha256() != right_initialization.rng_sha256() {
1032 restore_mismatches.push("rng");
1033 }
1034 if left_initialization.initial_state_sha256() != right_initialization.initial_state_sha256()
1035 {
1036 restore_mismatches.push("initial_state");
1037 }
1038 if !restore_mismatches.is_empty() {
1039 return Err(FerrumError::backend(format!(
1040 "{comparison_kind} restored different input/RNG/initial-state bytes between {} and {}: mismatch_fields={} left_restore_sha256={} right_restore_sha256={} left_input_sha256={} right_input_sha256={} left_rng_sha256={} right_rng_sha256={} left_initial_state_sha256={} right_initial_state_sha256={}",
1041 left.execution_id(),
1042 right.execution_id(),
1043 restore_mismatches.join(","),
1044 left.restore_sha256(),
1045 right.restore_sha256(),
1046 left_initialization.input_sha256(),
1047 right_initialization.input_sha256(),
1048 left_initialization.rng_sha256(),
1049 right_initialization.rng_sha256(),
1050 left_initialization.initial_state_sha256(),
1051 right_initialization.initial_state_sha256(),
1052 )));
1053 }
1054 if left.witnesses().len() != right.witnesses().len() {
1055 return Err(FerrumError::backend(format!(
1056 "{comparison_kind} witness cardinality differs between {} ({}) and {} ({})",
1057 left.execution_id(),
1058 left.witnesses().len(),
1059 right.execution_id(),
1060 right.witnesses().len()
1061 )));
1062 }
1063 for (left_witness, right_witness) in left.witnesses().iter().zip(right.witnesses()) {
1064 if left_witness != right_witness {
1065 return Err(witness_mismatch(
1066 comparison_kind,
1067 left.execution_id(),
1068 right.execution_id(),
1069 left_witness,
1070 right_witness,
1071 ));
1072 }
1073 }
1074 Ok(())
1075 }
1076
1077 fn witness_mismatch(
1078 comparison_kind: &str,
1079 left_execution_id: &str,
1080 right_execution_id: &str,
1081 left: &SubmissionWaveDeterminismArtifactWitness,
1082 right: &SubmissionWaveDeterminismArtifactWitness,
1083 ) -> FerrumError {
1084 FerrumError::backend(format!(
1085 "{comparison_kind} first witness mismatch between {left_execution_id} and {right_execution_id}: left=({},{},{},{},{},{},{},{},{},{}) right=({},{},{},{},{},{},{},{},{},{})",
1086 left.kind(),
1087 left.semantic_id(),
1088 left.node_id(),
1089 left.resource_id(),
1090 left.access(),
1091 left.participant_index(),
1092 left.logical_offset_bytes(),
1093 left.length_bytes(),
1094 left.element_type(),
1095 left.raw_sha256(),
1096 right.kind(),
1097 right.semantic_id(),
1098 right.node_id(),
1099 right.resource_id(),
1100 right.access(),
1101 right.participant_index(),
1102 right.logical_offset_bytes(),
1103 right.length_bytes(),
1104 right.element_type(),
1105 right.raw_sha256(),
1106 ))
1107 }
1108
1109 fn ensure_poison_equivalence(zero: &PendingCase, a5: &PendingCase) -> Result<()> {
1110 let zero_execution = zero.executions.first().ok_or_else(|| {
1111 FerrumError::internal("zero-poison determinism case contains no execution")
1112 })?;
1113 let a5_execution = a5.executions.first().ok_or_else(|| {
1114 FerrumError::internal("a5-poison determinism case contains no execution")
1115 })?;
1116 ensure_execution_equivalence(zero_execution, a5_execution, "workspace_poison").map_err(
1117 |error| {
1118 FerrumError::backend(format!(
1119 "workspace poison changed {} versus {}: {error}",
1120 zero.case_id, a5.case_id
1121 ))
1122 },
1123 )
1124 }
1125
1126 #[allow(clippy::too_many_arguments)]
1127 fn stage_collection(
1128 stage: &Path,
1129 collected_models: Vec<CollectedModel>,
1130 denominator: &ExecutionDeterminismEvidenceDenominator,
1131 denominator_bytes: &[u8],
1132 denominator_fingerprint: &str,
1133 device_runtime_fingerprint: &str,
1134 device_fingerprint: &str,
1135 scope: VNextDeterminismScope,
1136 expected_case_count: usize,
1137 models_lock: FileReference,
1138 hardware_probe: FileReference,
1139 binary: FileReference,
1140 ) -> Result<CollectorManifest> {
1141 let denominator_path = stage.join("denominator.json");
1142 write_bytes_exclusive(&denominator_path, denominator_bytes)?;
1143 let denominator_ref = DenominatorReference {
1144 path: "denominator.json".to_owned(),
1145 sha256: file_sha256(&denominator_path)?,
1146 size_bytes: file_size(&denominator_path)?,
1147 fingerprint: denominator_fingerprint.to_owned(),
1148 };
1149 if denominator_ref.sha256 != denominator_ref.fingerprint {
1150 return Err(FerrumError::internal(
1151 "typed denominator fingerprint differs from exact serialized bytes",
1152 ));
1153 }
1154
1155 let binary_sha256 = binary.sha256.clone();
1156 let mut case_refs = Vec::with_capacity(expected_case_count);
1157 let mut model_summaries = Vec::with_capacity(collected_models.len());
1158 let mut execution_count = 0;
1159 let mut comparison_count = 0;
1160 for model in collected_models {
1161 let identity = denominator
1162 .coverage()
1163 .models()
1164 .iter()
1165 .find(|identity| identity.model_key() == model.key)
1166 .ok_or_else(|| {
1167 FerrumError::internal(format!(
1168 "typed denominator lacks model identity {}",
1169 model.key
1170 ))
1171 })?;
1172 let targets = coverage_targets(denominator, &model.key)?;
1173 let case_count = model.cases.len();
1174 model_summaries.push(CollectorModelSummary {
1175 model_key: model.key.clone(),
1176 model_dir: model.directory.display().to_string(),
1177 resolved_plan_fingerprint: identity.resolved_plan_fingerprint().to_owned(),
1178 plan_hash: identity.plan_hash().as_str().to_owned(),
1179 dtype: model.dtype,
1180 quantization: model.quantization,
1181 case_count,
1182 });
1183 for pending in model.cases {
1184 let first = pending.executions.first().ok_or_else(|| {
1185 FerrumError::internal(format!(
1186 "determinism case {} contains no executions",
1187 pending.case_id
1188 ))
1189 })?;
1190 let initialization_input_sha256 =
1191 first.initialization_identity().input_sha256().to_owned();
1192 let initialization_rng_sha256 =
1193 first.initialization_identity().rng_sha256().to_owned();
1194 let initialization_state_sha256 = first
1195 .initialization_identity()
1196 .initial_state_sha256()
1197 .to_owned();
1198 let case = CaseArtifact {
1199 schema_version: 2,
1200 case_id: pending.case_id.clone(),
1201 denominator_fingerprint: denominator_fingerprint.to_owned(),
1202 binary_sha256: binary_sha256.clone(),
1203 device_runtime_implementation_fingerprint: device_runtime_fingerprint
1204 .to_owned(),
1205 device_fingerprint: device_fingerprint.to_owned(),
1206 model_key: pending.model_key,
1207 resolved_plan_fingerprint: identity.resolved_plan_fingerprint().to_owned(),
1208 plan_hash: identity.plan_hash().as_str().to_owned(),
1209 phase: pending.phase,
1210 token_shape: pending.token_shape,
1211 dtype: pending.dtype,
1212 quantization: pending.quantization,
1213 initialization: InitializationArtifact {
1214 input_sha256: initialization_input_sha256,
1215 rng_sha256: initialization_rng_sha256,
1216 initial_state_kind: pending.initial_state_kind,
1217 initial_state_sha256: initialization_state_sha256,
1218 workspace_poison: pending.workspace_poison,
1219 },
1220 coverage_targets: targets.clone(),
1221 executions: pending.executions,
1222 comparisons: pending.comparisons,
1223 first_mismatch: None,
1224 };
1225 execution_count += case.executions.len();
1226 comparison_count += case.comparisons.len();
1227 let relative = format!("cases/{}.json", case.case_id);
1228 let staged_path = stage.join(&relative);
1229 write_json_exclusive(&staged_path, &case)?;
1230 case_refs.push(FileReference {
1231 path: relative,
1232 sha256: file_sha256(&staged_path)?,
1233 size_bytes: file_size(&staged_path)?,
1234 });
1235 }
1236 }
1237 case_refs.sort_by(|left, right| left.path.cmp(&right.path));
1238 if case_refs.len() != expected_case_count {
1239 return Err(FerrumError::internal(format!(
1240 "determinism collector produced {} cases, expected {expected_case_count}",
1241 case_refs.len(),
1242 )));
1243 }
1244 let pass_prefix = match scope {
1245 VNextDeterminismScope::ReleaseFull => "FERRUM VNEXT DETERMINISM COLLECTOR PASS",
1246 VNextDeterminismScope::M1S2Focused => {
1247 "FERRUM VNEXT M1 S2 FOCUSED DETERMINISM COLLECTOR PASS"
1248 }
1249 };
1250 let pass_line = format!(
1251 "{pass_prefix}: {}",
1252 stage
1253 .parent()
1254 .expect("stage is inside artifact root")
1255 .display()
1256 );
1257 let manifest = CollectorManifest {
1258 schema_version: 1,
1259 artifact_type: ARTIFACT_TYPE,
1260 status: "pass",
1261 backend: "cuda",
1262 scope: scope.as_str(),
1263 models_lock,
1264 hardware_probe,
1265 device_fingerprint: device_fingerprint.to_owned(),
1266 binary,
1267 denominator: denominator_ref,
1268 models: model_summaries,
1269 cases: case_refs,
1270 case_count: expected_case_count,
1271 execution_count,
1272 comparison_count,
1273 pass_line,
1274 };
1275 write_json_exclusive(&stage.join("collector.json"), &manifest)?;
1276 Ok(manifest)
1277 }
1278
1279 fn coverage_targets(
1280 denominator: &ExecutionDeterminismEvidenceDenominator,
1281 model_key: &str,
1282 ) -> Result<Vec<CoverageTargetArtifact>> {
1283 let mut targets = Vec::new();
1284 let mut replay_equivalence = None;
1285 for requirement in denominator.coverage().provider_requirements() {
1286 let Some(selection) = requirement
1287 .model_selections()
1288 .iter()
1289 .find(|selection| selection.model_key() == model_key)
1290 else {
1291 continue;
1292 };
1293 let evidence = denominator
1294 .provider_evidence()
1295 .iter()
1296 .find(|evidence| {
1297 evidence.model_key() == model_key
1298 && evidence.operation_id() == requirement.operation_id()
1299 && evidence.provider_id() == requirement.provider_id()
1300 })
1301 .ok_or_else(|| {
1302 FerrumError::internal(format!(
1303 "typed denominator lacks provider evidence for {model_key}/{}/{}",
1304 requirement.operation_id(),
1305 requirement.provider_id()
1306 ))
1307 })?;
1308 match replay_equivalence {
1309 None => replay_equivalence = Some(requirement.replay_equivalence()),
1310 Some(expected) if expected != requirement.replay_equivalence() => {
1311 return Err(FerrumError::unsupported(format!(
1312 "model {model_key} mixes replay equivalence contracts inside one full-program determinism wave"
1313 )));
1314 }
1315 Some(_) => {}
1316 }
1317 targets.push(CoverageTargetArtifact {
1318 operation_id: requirement.operation_id().to_string(),
1319 operation_version: requirement.operation_version(),
1320 operation_fingerprint: requirement.operation_fingerprint().to_owned(),
1321 provider_id: requirement.provider_id().to_string(),
1322 provider_version: requirement.provider_version(),
1323 provider_implementation_fingerprint: requirement
1324 .provider_implementation_fingerprint()
1325 .to_owned(),
1326 provider_execution_contract_fingerprint: requirement
1327 .provider_execution_contract_fingerprint()
1328 .to_string(),
1329 replay_equivalence: requirement.replay_equivalence().as_str().to_owned(),
1330 witness_plan_fingerprint: evidence.witness_plan_fingerprint().to_owned(),
1331 node_ids: selection
1332 .node_ids()
1333 .iter()
1334 .map(ToString::to_string)
1335 .collect(),
1336 });
1337 }
1338 if targets.is_empty() {
1339 return Err(FerrumError::internal(format!(
1340 "typed denominator selected no provider targets for model {model_key}"
1341 )));
1342 }
1343 if replay_equivalence != Some(ProviderReplayEquivalence::BitwiseEagerEquivalent) {
1344 return Err(FerrumError::unsupported(format!(
1345 "model {model_key} does not authorize bitwise eager/replay comparison for every selected provider"
1346 )));
1347 }
1348 targets.sort_by(|left, right| {
1349 (&left.operation_id, &left.provider_id).cmp(&(&right.operation_id, &right.provider_id))
1350 });
1351 Ok(targets)
1352 }
1353
1354 fn reject_existing_outputs(root: &Path) -> Result<()> {
1355 for relative in [
1356 "denominator.json",
1357 "cases",
1358 "collector.json",
1359 "collector.reject.json",
1360 "collector-progress",
1361 ] {
1362 let path = root.join(relative);
1363 if path.exists() {
1364 return Err(FerrumError::invalid_parameter(format!(
1365 "determinism output already exists and will not be overwritten: {}",
1366 path.display()
1367 )));
1368 }
1369 }
1370 Ok(())
1371 }
1372
1373 fn read_hardware_probe(path: &Path) -> Result<HardwareProbeIdentity> {
1374 let bytes = fs::read(path).map_err(|error| {
1375 FerrumError::io(format!(
1376 "cannot read CUDA hardware probe {}: {error}",
1377 path.display()
1378 ))
1379 })?;
1380 let probe: HardwareProbeIdentity = serde_json::from_slice(&bytes).map_err(|error| {
1381 FerrumError::serialization(format!(
1382 "cannot decode CUDA hardware probe {}: {error}",
1383 path.display()
1384 ))
1385 })?;
1386 if probe.schema_version != 1 || !is_sha256(&probe.fingerprint) {
1387 return Err(FerrumError::invalid_parameter(
1388 "CUDA hardware probe identity is not schema v1 with a lowercase SHA256",
1389 ));
1390 }
1391 Ok(probe)
1392 }
1393
1394 fn publish_stage(stage: &Path, root: &Path) -> Result<()> {
1395 for relative in ["denominator.json", "cases", "collector.json"] {
1396 let source = stage.join(relative);
1397 let destination = root.join(relative);
1398 fs::rename(&source, &destination).map_err(|error| {
1399 FerrumError::io(format!(
1400 "cannot publish determinism artifact {} to {}: {error}",
1401 source.display(),
1402 destination.display()
1403 ))
1404 })?;
1405 }
1406 fs::remove_dir(stage).map_err(|error| {
1407 FerrumError::io(format!(
1408 "cannot remove empty determinism staging directory {}: {error}",
1409 stage.display()
1410 ))
1411 })?;
1412 Ok(())
1413 }
1414
1415 fn file_reference(root: &Path, path: &Path, expected_relative: &str) -> Result<FileReference> {
1416 let canonical_root = root.canonicalize().map_err(|error| {
1417 FerrumError::io(format!(
1418 "cannot canonicalize artifact root {}: {error}",
1419 root.display()
1420 ))
1421 })?;
1422 let canonical_path = path.canonicalize().map_err(|error| {
1423 FerrumError::io(format!("cannot canonicalize {}: {error}", path.display()))
1424 })?;
1425 let relative = canonical_path
1426 .strip_prefix(&canonical_root)
1427 .map_err(|_| {
1428 FerrumError::invalid_parameter(format!(
1429 "artifact input must be inside artifact root: {}",
1430 path.display()
1431 ))
1432 })?
1433 .to_string_lossy()
1434 .replace('\\', "/");
1435 if relative != expected_relative {
1436 return Err(FerrumError::invalid_parameter(format!(
1437 "artifact input path must be {expected_relative}, got {relative}"
1438 )));
1439 }
1440 Ok(FileReference {
1441 path: relative,
1442 sha256: file_sha256(path)?,
1443 size_bytes: file_size(path)?,
1444 })
1445 }
1446
1447 fn absolute_file_reference(path: &Path) -> Result<FileReference> {
1448 Ok(FileReference {
1449 path: path.display().to_string(),
1450 sha256: file_sha256(path)?,
1451 size_bytes: file_size(path)?,
1452 })
1453 }
1454
1455 fn file_size(path: &Path) -> Result<u64> {
1456 path.metadata()
1457 .map(|metadata| metadata.len())
1458 .map_err(|error| FerrumError::io(format!("cannot stat {}: {error}", path.display())))
1459 }
1460
1461 fn file_sha256(path: &Path) -> Result<String> {
1462 let mut file = File::open(path)
1463 .map_err(|error| FerrumError::io(format!("cannot open {}: {error}", path.display())))?;
1464 let mut digest = Sha256::new();
1465 let mut buffer = [0_u8; 1024 * 1024];
1466 loop {
1467 let read = file.read(&mut buffer).map_err(|error| {
1468 FerrumError::io(format!("cannot hash {}: {error}", path.display()))
1469 })?;
1470 if read == 0 {
1471 break;
1472 }
1473 digest.update(&buffer[..read]);
1474 }
1475 Ok(format!("{:x}", digest.finalize()))
1476 }
1477
1478 fn write_json_exclusive(path: &Path, value: &impl Serialize) -> Result<()> {
1479 let mut bytes = serde_json::to_vec_pretty(value)
1480 .map_err(|error| FerrumError::serialization(error.to_string()))?;
1481 bytes.push(b'\n');
1482 write_bytes_exclusive(path, &bytes)
1483 }
1484
1485 fn write_bytes_exclusive(path: &Path, bytes: &[u8]) -> Result<()> {
1486 let mut file = OpenOptions::new()
1487 .write(true)
1488 .create_new(true)
1489 .open(path)
1490 .map_err(|error| {
1491 FerrumError::io(format!("cannot create {}: {error}", path.display()))
1492 })?;
1493 file.write_all(bytes).map_err(|error| {
1494 FerrumError::io(format!("cannot write {}: {error}", path.display()))
1495 })?;
1496 file.sync_all()
1497 .map_err(|error| FerrumError::io(format!("cannot sync {}: {error}", path.display())))
1498 }
1499
1500 fn is_sha256(value: &str) -> bool {
1501 value.len() == 64
1502 && value
1503 .bytes()
1504 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
1505 }
1506
1507 fn vnext_backend_error(error: VNextError) -> FerrumError {
1508 FerrumError::backend(error.to_string())
1509 }
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514 use super::*;
1515
1516 #[test]
1517 fn model_bindings_are_exact_and_canonical() {
1518 let values = vec![
1519 "m3-qwen3-30b-a3b=/models/m3".to_owned(),
1520 "m1-qwen35-4b=/models/m1".to_owned(),
1521 "m2-qwen35-35b-a3b=/models/m2".to_owned(),
1522 ];
1523 let bindings =
1524 parse_model_bindings(&values, VNextDeterminismScope::ReleaseFull, false).unwrap();
1525 assert_eq!(
1526 bindings
1527 .iter()
1528 .map(|binding| binding.key.as_str())
1529 .collect::<Vec<_>>(),
1530 PRIMARY_MODEL_KEYS
1531 );
1532 }
1533
1534 #[test]
1535 fn model_bindings_reject_missing_duplicate_and_unknown_models() {
1536 let missing = vec![
1537 "m1-qwen35-4b=/models/m1".to_owned(),
1538 "m2-qwen35-35b-a3b=/models/m2".to_owned(),
1539 ];
1540 assert!(parse_model_bindings(&missing, VNextDeterminismScope::ReleaseFull, false).is_err());
1541
1542 let duplicate = vec![
1543 "m1-qwen35-4b=/models/m1".to_owned(),
1544 "m1-qwen35-4b=/models/other".to_owned(),
1545 "m2-qwen35-35b-a3b=/models/m2".to_owned(),
1546 "m3-qwen3-30b-a3b=/models/m3".to_owned(),
1547 ];
1548 assert!(
1549 parse_model_bindings(&duplicate, VNextDeterminismScope::ReleaseFull, false).is_err()
1550 );
1551
1552 let unknown = vec![
1553 "m1-qwen35-4b=/models/m1".to_owned(),
1554 "m2-qwen35-35b-a3b=/models/m2".to_owned(),
1555 "llama=/models/llama".to_owned(),
1556 ];
1557 assert!(parse_model_bindings(&unknown, VNextDeterminismScope::ReleaseFull, false).is_err());
1558 }
1559
1560 #[test]
1561 fn focused_scope_accepts_only_m1_and_cannot_bind_release_models() {
1562 assert_eq!(
1563 VNextDeterminismScope::from_str("m1-s2-focused", false).unwrap(),
1564 VNextDeterminismScope::M1S2Focused
1565 );
1566 assert!(VNextDeterminismScope::from_str("m1s2-focused", false).is_err());
1567
1568 let m1 = vec!["m1-qwen35-4b=/models/m1".to_owned()];
1569 let bindings =
1570 parse_model_bindings(&m1, VNextDeterminismScope::M1S2Focused, false).unwrap();
1571 assert_eq!(bindings.len(), 1);
1572 assert_eq!(bindings[0].key, "m1-qwen35-4b");
1573
1574 let release = vec![
1575 "m1-qwen35-4b=/models/m1".to_owned(),
1576 "m2-qwen35-35b-a3b=/models/m2".to_owned(),
1577 "m3-qwen3-30b-a3b=/models/m3".to_owned(),
1578 ];
1579 assert!(parse_model_bindings(&release, VNextDeterminismScope::M1S2Focused, false).is_err());
1580 }
1581
1582 #[test]
1583 fn determinism_fixtures_are_valid_product_inputs() {
1584 for scope in [
1585 VNextDeterminismScope::ReleaseFull,
1586 VNextDeterminismScope::M1S2Focused,
1587 ] {
1588 let fixtures = shape_fixtures(scope);
1589 assert!(!fixtures.is_empty());
1590 assert!(fixtures.iter().all(|fixture| fixture
1591 .participants
1592 .iter()
1593 .all(|participant| participant.to_spec().is_ok())));
1594 }
1595 }
1596}