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, VNextDeterminismPhase,
15};
16#[cfg(any(feature = "cuda", test))]
17use ferrum_models::{
18 VNextDeterminismInitialState, VNextDeterminismParticipantSpec, VNextDeterminismWorkspacePoison,
19 MAX_VNEXT_DETERMINISM_PARTICIPANTS,
20};
21use ferrum_types::{FerrumError, Result};
22
23const PRIMARY_MODEL_KEYS: [&str; 3] = ["m1-qwen35-4b", "m2-qwen35-35b-a3b", "m3-qwen3-30b-a3b"];
24const M1_MODEL_KEYS: [&str; 1] = ["m1-qwen35-4b"];
25#[cfg(feature = "cuda")]
26const EXECUTIONS_PER_MODE: usize = 6;
27#[cfg(any(feature = "cuda", test))]
28const RELEASE_EXPECTED_CASES: usize = 72;
29#[cfg(any(feature = "cuda", test))]
30const M1_S2_FOCUSED_EXPECTED_CASES: usize = 20;
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
33pub enum VNextDeterminismScope {
34 #[default]
35 #[value(name = "release-full")]
36 ReleaseFull,
37 #[value(name = "m1-s2-focused")]
38 M1S2Focused,
39}
40
41impl VNextDeterminismScope {
42 const fn as_str(self) -> &'static str {
43 match self {
44 Self::ReleaseFull => "release-full",
45 Self::M1S2Focused => "m1-s2-focused",
46 }
47 }
48
49 const fn model_keys(self) -> &'static [&'static str] {
50 match self {
51 Self::ReleaseFull => &PRIMARY_MODEL_KEYS,
52 Self::M1S2Focused => &M1_MODEL_KEYS,
53 }
54 }
55
56 #[cfg(any(feature = "cuda", test))]
57 const fn expected_case_count(self) -> usize {
58 match self {
59 Self::ReleaseFull => RELEASE_EXPECTED_CASES,
60 Self::M1S2Focused => M1_S2_FOCUSED_EXPECTED_CASES,
61 }
62 }
63}
64
65#[derive(Args, Clone, Debug)]
66pub struct VNextDeterminismCommand {
67 #[arg(long, value_name = "PATH")]
69 pub models_lock: PathBuf,
70
71 #[arg(long, value_name = "DIR")]
73 pub artifact_root: PathBuf,
74
75 #[arg(long, value_enum, default_value_t = VNextDeterminismScope::ReleaseFull)]
77 pub scope: VNextDeterminismScope,
78
79 #[arg(
81 long = "model",
82 value_name = "MODEL_KEY=DIR",
83 action = clap::ArgAction::Append
84 )]
85 pub models: Vec<String>,
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
89struct ModelBinding {
90 key: String,
91 directory: PathBuf,
92}
93
94#[cfg(any(feature = "cuda", test))]
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96enum ShapePhase {
97 Prefill,
98 Decode,
99}
100
101#[cfg(any(feature = "cuda", test))]
102impl ShapePhase {
103 const fn as_str(self) -> &'static str {
104 match self {
105 Self::Prefill => "prefill",
106 Self::Decode => "decode",
107 }
108 }
109
110 #[cfg(feature = "cuda")]
111 const fn execution_phase(self) -> VNextDeterminismPhase {
112 match self {
113 Self::Prefill => VNextDeterminismPhase::Prefill,
114 Self::Decode => VNextDeterminismPhase::Decode,
115 }
116 }
117}
118
119#[cfg(any(feature = "cuda", test))]
120#[derive(Clone, Debug, PartialEq, Eq)]
121struct ParticipantFixture {
122 token_ids: Vec<u32>,
123 immediate_start: usize,
124}
125
126#[cfg(any(feature = "cuda", test))]
127impl ParticipantFixture {
128 fn immediate_end(&self) -> usize {
129 self.token_ids.len()
130 }
131
132 fn to_spec(&self) -> Result<VNextDeterminismParticipantSpec> {
133 VNextDeterminismParticipantSpec::new(
134 self.token_ids.clone(),
135 self.immediate_start..self.immediate_end(),
136 self.immediate_end().saturating_add(8),
137 )
138 }
139}
140
141#[cfg(any(feature = "cuda", test))]
142#[derive(Clone, Debug, PartialEq, Eq)]
143struct ShapeFixture {
144 phase: ShapePhase,
145 partition: &'static str,
146 participants: Vec<ParticipantFixture>,
147}
148
149#[cfg(any(feature = "cuda", test))]
150impl ShapeFixture {
151 #[cfg(feature = "cuda")]
152 fn execution_spec(
153 &self,
154 initial_state: VNextDeterminismInitialState,
155 workspace_poison: VNextDeterminismWorkspacePoison,
156 mode: VNextDeterminismExecutionMode,
157 ) -> Result<VNextDeterminismExecutionSpec> {
158 VNextDeterminismExecutionSpec::new(
159 self.phase.execution_phase(),
160 self.participants
161 .iter()
162 .map(ParticipantFixture::to_spec)
163 .collect::<Result<Vec<_>>>()?,
164 initial_state,
165 workspace_poison,
166 mode,
167 )
168 }
169}
170
171pub async fn execute(command: VNextDeterminismCommand) -> Result<()> {
172 let bindings = validate_command(&command)?;
173 #[cfg(feature = "cuda")]
174 {
175 return cuda::collect(command, bindings).await;
176 }
177 #[cfg(not(feature = "cuda"))]
178 {
179 let _ = bindings;
180 Err(FerrumError::unsupported(
181 "ferrum vnext-determinism requires a binary built with the cuda feature",
182 ))
183 }
184}
185
186fn validate_command(command: &VNextDeterminismCommand) -> Result<Vec<ModelBinding>> {
187 require_regular_file(&command.models_lock, "--models-lock")?;
188 if !command.artifact_root.is_dir() {
189 return Err(FerrumError::invalid_parameter(format!(
190 "--artifact-root is not a directory: {}",
191 command.artifact_root.display()
192 )));
193 }
194 require_regular_file(
195 &command.artifact_root.join("hardware-probe/probe.json"),
196 "hardware-probe/probe.json",
197 )?;
198 parse_model_bindings(&command.models, command.scope, true)
199}
200
201fn require_regular_file(path: &Path, label: &str) -> Result<()> {
202 let metadata = path.symlink_metadata().map_err(|error| {
203 FerrumError::invalid_parameter(format!(
204 "{label} is not a readable regular file at {}: {error}",
205 path.display()
206 ))
207 })?;
208 if metadata.file_type().is_symlink() || !metadata.is_file() {
209 return Err(FerrumError::invalid_parameter(format!(
210 "{label} must be a real regular file: {}",
211 path.display()
212 )));
213 }
214 Ok(())
215}
216
217fn parse_model_bindings(
218 values: &[String],
219 scope: VNextDeterminismScope,
220 require_directories: bool,
221) -> Result<Vec<ModelBinding>> {
222 let model_keys = scope.model_keys();
223 let expected = model_keys.iter().copied().collect::<BTreeSet<_>>();
224 let mut indexed = BTreeMap::new();
225 for value in values {
226 let (key, raw_directory) = value.split_once('=').ok_or_else(|| {
227 FerrumError::invalid_parameter(format!(
228 "--model must use MODEL_KEY=DIR form, got {value:?}"
229 ))
230 })?;
231 if key.is_empty() || raw_directory.is_empty() || !expected.contains(key) {
232 return Err(FerrumError::invalid_parameter(format!(
233 "--model has an unknown key or empty directory: {value:?}"
234 )));
235 }
236 let directory = PathBuf::from(raw_directory);
237 if require_directories && (!directory.is_absolute() || !directory.is_dir()) {
238 return Err(FerrumError::invalid_parameter(format!(
239 "--model {key} must name an existing absolute directory: {}",
240 directory.display()
241 )));
242 }
243 if indexed.insert(key.to_owned(), directory).is_some() {
244 return Err(FerrumError::invalid_parameter(format!(
245 "--model duplicates primary model key {key}"
246 )));
247 }
248 }
249 let actual = indexed.keys().map(String::as_str).collect::<BTreeSet<_>>();
250 if actual != expected {
251 let missing = expected.difference(&actual).copied().collect::<Vec<_>>();
252 return Err(FerrumError::invalid_parameter(format!(
253 "--model must bind exactly the {} model set for scope {}; missing {missing:?}",
254 model_keys.len(),
255 scope.as_str(),
256 )));
257 }
258 Ok(model_keys
259 .iter()
260 .map(|key| ModelBinding {
261 key: (*key).to_owned(),
262 directory: indexed
263 .remove(*key)
264 .expect("exact primary model set was checked"),
265 })
266 .collect())
267}
268
269#[cfg(any(feature = "cuda", test))]
270fn shape_fixtures(scope: VNextDeterminismScope) -> Vec<ShapeFixture> {
271 let prefill = |partition, token_count, immediate_start| ShapeFixture {
272 phase: ShapePhase::Prefill,
273 partition,
274 participants: vec![ParticipantFixture {
275 token_ids: deterministic_tokens(0, token_count),
276 immediate_start,
277 }],
278 };
279 let decode = |partition, participant_count| ShapeFixture {
280 phase: ShapePhase::Decode,
281 partition,
282 participants: (0..participant_count)
283 .map(|participant| ParticipantFixture {
284 token_ids: deterministic_tokens(participant, 9),
285 immediate_start: 8,
286 })
287 .collect(),
288 };
289 let mut fixtures = vec![
290 prefill("single_token", 1, 0),
291 prefill("multi_token", 4, 0),
292 prefill("chunk_boundary", 8, 4),
293 decode("c1", 1),
294 decode("multi_participant", 4),
295 ];
296 if scope == VNextDeterminismScope::ReleaseFull {
297 fixtures.push(decode("c32", MAX_VNEXT_DETERMINISM_PARTICIPANTS));
298 }
299 fixtures
300}
301
302#[cfg(any(feature = "cuda", test))]
303fn deterministic_tokens(participant: usize, count: usize) -> Vec<u32> {
304 let base = 100_u32.saturating_add(
305 u32::try_from(participant)
306 .unwrap_or(u32::MAX)
307 .saturating_mul(16),
308 );
309 (0..count)
310 .map(|offset| base.saturating_add(u32::try_from(offset).unwrap_or(u32::MAX)))
311 .collect()
312}
313
314#[cfg(feature = "cuda")]
315mod cuda {
316 use std::fs::{self, File, OpenOptions};
317 use std::io::{Read, Write};
318 use std::sync::Arc;
319
320 use ferrum_engine::vnext_determinism::{
321 create_cuda_vnext_determinism_collector, CudaVNextDeterminismCollector,
322 };
323 use ferrum_interfaces::vnext::{
324 CapabilityCatalog, ContractVersion, ExecutionDeterminismEvidenceDenominator,
325 ExecutionDeterminismProviderCoverage, ProviderReplayEquivalence, ResolvedModelPlan,
326 SubmissionWaveDeterminismArtifactExecution, SubmissionWaveDeterminismArtifactWitness,
327 VNextError,
328 };
329 use ferrum_models::vnext::{
330 open_registered_colocated_safetensors, resolve_registered_model_from_sources,
331 PreparedProductionModel,
332 };
333 use ferrum_types::{Device, EngineConfig, ModelId};
334 use serde::{Deserialize, Serialize};
335 use sha2::{Digest, Sha256};
336 use uuid::Uuid;
337
338 use super::*;
339
340 const ARTIFACT_TYPE: &str = "runtime_vnext_cuda_determinism_collector";
341
342 #[derive(Debug, Serialize)]
343 struct TokenShapeArtifact {
344 partition: String,
345 participant_count: usize,
346 immediate_tokens: Vec<usize>,
347 source_start_tokens: Vec<usize>,
348 source_end_tokens: Vec<usize>,
349 }
350
351 impl TokenShapeArtifact {
352 fn from_fixture(fixture: &ShapeFixture) -> Self {
353 Self {
354 partition: fixture.partition.to_owned(),
355 participant_count: fixture.participants.len(),
356 immediate_tokens: fixture
357 .participants
358 .iter()
359 .map(|participant| {
360 participant
361 .immediate_end()
362 .saturating_sub(participant.immediate_start)
363 })
364 .collect(),
365 source_start_tokens: fixture
366 .participants
367 .iter()
368 .map(|participant| participant.immediate_start)
369 .collect(),
370 source_end_tokens: fixture
371 .participants
372 .iter()
373 .map(ParticipantFixture::immediate_end)
374 .collect(),
375 }
376 }
377 }
378
379 #[derive(Debug, Serialize)]
380 struct InitializationArtifact {
381 input_sha256: String,
382 rng_sha256: String,
383 initial_state_kind: String,
384 initial_state_sha256: String,
385 workspace_poison: String,
386 }
387
388 #[derive(Clone, Debug, Serialize)]
389 struct CoverageTargetArtifact {
390 operation_id: String,
391 operation_version: ContractVersion,
392 operation_fingerprint: String,
393 provider_id: String,
394 provider_version: ContractVersion,
395 provider_implementation_fingerprint: String,
396 provider_execution_contract_fingerprint: String,
397 replay_equivalence: String,
398 witness_plan_fingerprint: String,
399 node_ids: Vec<String>,
400 }
401
402 #[derive(Debug, Serialize)]
403 struct ComparisonArtifact {
404 kind: String,
405 ordinal: usize,
406 left_execution_id: String,
407 right_execution_id: String,
408 relation: &'static str,
409 first_mismatch: Option<String>,
410 }
411
412 #[derive(Debug, Serialize)]
413 struct CaseArtifact {
414 schema_version: u32,
415 case_id: String,
416 denominator_fingerprint: String,
417 binary_sha256: String,
418 device_runtime_implementation_fingerprint: String,
419 device_fingerprint: String,
420 model_key: String,
421 resolved_plan_fingerprint: String,
422 plan_hash: String,
423 phase: String,
424 token_shape: TokenShapeArtifact,
425 dtype: String,
426 quantization: String,
427 initialization: InitializationArtifact,
428 coverage_targets: Vec<CoverageTargetArtifact>,
429 executions: Vec<SubmissionWaveDeterminismArtifactExecution>,
430 comparisons: Vec<ComparisonArtifact>,
431 first_mismatch: Option<String>,
432 }
433
434 struct PendingCase {
435 case_id: String,
436 model_key: String,
437 phase: String,
438 token_shape: TokenShapeArtifact,
439 dtype: String,
440 quantization: String,
441 initial_state_kind: String,
442 workspace_poison: String,
443 executions: Vec<SubmissionWaveDeterminismArtifactExecution>,
444 comparisons: Vec<ComparisonArtifact>,
445 }
446
447 #[derive(Debug, Serialize)]
448 struct CaseProgressArtifact<'a> {
449 schema_version: u32,
450 artifact_type: &'static str,
451 status: &'static str,
452 case_id: &'a str,
453 model_key: &'a str,
454 phase: &'a str,
455 token_shape: &'a TokenShapeArtifact,
456 dtype: &'a str,
457 quantization: &'a str,
458 initial_state_kind: &'a str,
459 workspace_poison: &'a str,
460 initialization_identity:
461 &'a ferrum_interfaces::vnext::SubmissionWaveDeterminismArtifactInitializationIdentity,
462 execution_count: usize,
463 comparison_count: usize,
464 canonical_witness_count: usize,
465 canonical_witnesses_sha256: String,
466 replayed_segment_count: usize,
467 }
468
469 struct CollectedModel {
470 key: String,
471 directory: PathBuf,
472 plan: ResolvedModelPlan,
473 dtype: String,
474 quantization: String,
475 cases: Vec<PendingCase>,
476 }
477
478 #[derive(Debug, Deserialize)]
479 struct HardwareProbeIdentity {
480 schema_version: u32,
481 fingerprint: String,
482 }
483
484 #[derive(Debug, Serialize)]
485 struct FileReference {
486 path: String,
487 sha256: String,
488 size_bytes: u64,
489 }
490
491 #[derive(Debug, Serialize)]
492 struct DenominatorReference {
493 path: String,
494 sha256: String,
495 size_bytes: u64,
496 fingerprint: String,
497 }
498
499 #[derive(Debug, Serialize)]
500 struct CollectorModelSummary {
501 model_key: String,
502 model_dir: String,
503 resolved_plan_fingerprint: String,
504 plan_hash: String,
505 dtype: String,
506 quantization: String,
507 case_count: usize,
508 }
509
510 #[derive(Debug, Serialize)]
511 struct CollectorManifest {
512 schema_version: u32,
513 artifact_type: &'static str,
514 status: &'static str,
515 backend: &'static str,
516 scope: &'static str,
517 models_lock: FileReference,
518 hardware_probe: FileReference,
519 device_fingerprint: String,
520 binary: FileReference,
521 denominator: DenominatorReference,
522 models: Vec<CollectorModelSummary>,
523 cases: Vec<FileReference>,
524 case_count: usize,
525 execution_count: usize,
526 comparison_count: usize,
527 pass_line: String,
528 }
529
530 #[derive(Debug, Serialize)]
531 struct RejectionArtifact {
532 schema_version: u32,
533 artifact_type: &'static str,
534 status: &'static str,
535 failure_class: &'static str,
536 message: String,
537 }
538
539 pub(super) async fn collect(
540 command: VNextDeterminismCommand,
541 bindings: Vec<ModelBinding>,
542 ) -> Result<()> {
543 match collect_inner(&command, &bindings).await {
544 Ok(pass_line) => {
545 println!("{pass_line}");
546 Ok(())
547 }
548 Err(error) => {
549 let rejection = RejectionArtifact {
550 schema_version: 1,
551 artifact_type: ARTIFACT_TYPE,
552 status: "reject",
553 failure_class: "collector_failure",
554 message: error.to_string(),
555 };
556 let rejection_path = command.artifact_root.join("collector.reject.json");
557 let _ = write_json_exclusive(&rejection_path, &rejection);
558 Err(error)
559 }
560 }
561 }
562
563 async fn collect_inner(
564 command: &VNextDeterminismCommand,
565 bindings: &[ModelBinding],
566 ) -> Result<String> {
567 let fixtures = shape_fixtures(command.scope);
568 let expected_case_count = command.scope.expected_case_count();
569 let cases_per_model = fixtures.len() * 4;
570 reject_existing_outputs(&command.artifact_root)?;
571 let models_lock = file_reference(
572 &command.artifact_root,
573 &command.models_lock,
574 "models.lock.json",
575 )?;
576 let probe_path = command.artifact_root.join("hardware-probe/probe.json");
577 let hardware_probe = read_hardware_probe(&probe_path)?;
578 let hardware_probe_ref = file_reference(
579 &command.artifact_root,
580 &probe_path,
581 "hardware-probe/probe.json",
582 )?;
583 let current_exe = std::env::current_exe().map_err(|error| {
584 FerrumError::io(format!("cannot resolve current ferrum binary: {error}"))
585 })?;
586 let binary = absolute_file_reference(¤t_exe)?;
587 let progress_root = command.artifact_root.join("collector-progress");
588 let progress_cases = progress_root.join("cases");
589 fs::create_dir(&progress_root).map_err(|error| {
590 FerrumError::io(format!(
591 "cannot create determinism progress directory {}: {error}",
592 progress_root.display()
593 ))
594 })?;
595 fs::create_dir(&progress_cases).map_err(|error| {
596 FerrumError::io(format!(
597 "cannot create determinism progress case directory {}: {error}",
598 progress_cases.display()
599 ))
600 })?;
601
602 let mut canonical_catalog: Option<CapabilityCatalog> = None;
603 let mut canonical_catalog_fingerprint: Option<String> = None;
604 let mut collected_models = Vec::with_capacity(bindings.len());
605 for (model_ordinal, binding) in bindings.iter().enumerate() {
606 println!(
607 "FERRUM VNEXT DETERMINISM PROGRESS model={} stage=load ordinal={}/{}",
608 binding.key,
609 model_ordinal + 1,
610 bindings.len()
611 );
612 let sources = Arc::new(
613 open_registered_colocated_safetensors(&binding.directory).map_err(|error| {
614 FerrumError::model(format!(
615 "cannot open registered source for {} at {}: {error}",
616 binding.key,
617 binding.directory.display()
618 ))
619 })?,
620 );
621 let registration = resolve_registered_model_from_sources(sources.as_ref())
622 .and_then(|registration| registration.into_required())
623 .map_err(|error| {
624 FerrumError::model(format!(
625 "cannot require vNext registration for {}: {error}",
626 binding.key
627 ))
628 })?;
629 let prepared = registration
630 .prepare_from_sources(sources)
631 .map_err(|error| {
632 FerrumError::model(format!(
633 "cannot prepare registered vNext model {}: {error}",
634 binding.key
635 ))
636 })?;
637 let capabilities = prepared.model_capabilities()?;
638 let dtype = prepared.descriptor().execution_dtype().to_string();
639 let quantization = capabilities
640 .quantization
641 .unwrap_or_else(|| "none".to_owned());
642 let mut engine = determinism_engine_config(binding, &prepared);
643 engine.backend.dtype = prepared.descriptor().execution_dtype();
644 let collector = create_cuda_vnext_determinism_collector(&engine, &prepared, 0)
645 .map_err(|error| {
646 FerrumError::backend(format!(
647 "cannot create CUDA determinism collector for {}: {error}",
648 binding.key
649 ))
650 })?;
651 let catalog_fingerprint = collector
652 .capability_catalog()
653 .fingerprint()
654 .map_err(vnext_backend_error)?;
655 match canonical_catalog_fingerprint.as_deref() {
656 Some(expected) if expected != catalog_fingerprint => {
657 return Err(FerrumError::backend(format!(
658 "CUDA capability catalog drifted between primary models: expected {expected}, got {catalog_fingerprint} for {}",
659 binding.key
660 )));
661 }
662 None => {
663 canonical_catalog = Some(collector.capability_catalog().clone());
664 canonical_catalog_fingerprint = Some(catalog_fingerprint);
665 }
666 Some(_) => {}
667 }
668 let plan = collector.resolved_model_plan().clone();
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: &PreparedProductionModel,
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 release_shapes_cover_the_exact_bounded_partition_matrix() {
1584 let fixtures = shape_fixtures(VNextDeterminismScope::ReleaseFull);
1585 assert_eq!(fixtures.len(), 6);
1586 assert_eq!(
1587 fixtures
1588 .iter()
1589 .map(|fixture| (fixture.phase.as_str(), fixture.partition))
1590 .collect::<BTreeSet<_>>(),
1591 BTreeSet::from([
1592 ("prefill", "single_token"),
1593 ("prefill", "multi_token"),
1594 ("prefill", "chunk_boundary"),
1595 ("decode", "c1"),
1596 ("decode", "multi_participant"),
1597 ("decode", "c32"),
1598 ])
1599 );
1600 assert_eq!(
1601 fixtures
1602 .iter()
1603 .find(|fixture| fixture.partition == "c32")
1604 .unwrap()
1605 .participants
1606 .len(),
1607 MAX_VNEXT_DETERMINISM_PARTICIPANTS
1608 );
1609 assert!(fixtures.iter().all(|fixture| fixture
1610 .participants
1611 .iter()
1612 .all(|participant| participant.to_spec().is_ok())));
1613 }
1614
1615 #[test]
1616 fn case_denominator_is_three_models_times_exact_fixture_cross_product() {
1617 let states = [
1618 VNextDeterminismInitialState::Zero,
1619 VNextDeterminismInitialState::Nonzero,
1620 ];
1621 let poisons = [
1622 VNextDeterminismWorkspacePoison::Zero,
1623 VNextDeterminismWorkspacePoison::A5,
1624 ];
1625 assert_eq!(
1626 PRIMARY_MODEL_KEYS.len()
1627 * shape_fixtures(VNextDeterminismScope::ReleaseFull).len()
1628 * states.len()
1629 * poisons.len(),
1630 RELEASE_EXPECTED_CASES
1631 );
1632 }
1633
1634 #[test]
1635 fn focused_denominator_is_m1_without_c32() {
1636 let fixtures = shape_fixtures(VNextDeterminismScope::M1S2Focused);
1637 assert_eq!(fixtures.len(), 5);
1638 assert!(fixtures.iter().all(|fixture| fixture.partition != "c32"));
1639 assert_eq!(
1640 M1_MODEL_KEYS.len() * fixtures.len() * 2 * 2,
1641 M1_S2_FOCUSED_EXPECTED_CASES
1642 );
1643 assert_eq!(
1644 VNextDeterminismScope::M1S2Focused.expected_case_count(),
1645 M1_S2_FOCUSED_EXPECTED_CASES
1646 );
1647 }
1648}