Fault Injection Evaluation Kit

The fault injection evaluation kit runs reproducible failure campaigns against PyTorch, TorchTitan, Megatron Core, and DeepSpeed pre-training workloads. It records verified ground truth and accepts neutral localization results, without depending on SCOUT, GEMINI, or another resiliency system.

Campaigns describe observable failure effects. Built-in hooks execute safe model and optimizer faults; callback executors connect the same schema to isolated process, storage, communication, and cluster controls. See the manual fault injection and SCOUT localization campaign for an eight-GPU DDP qualification run that compares injection and localization JSON artifacts.

Enable a Campaign

Enable the campaign once after framework initialization and before training:

from lm_resiliency import enable_fault_injection

faults = enable_fault_injection(
    model,
    optimizer,
    campaign=campaign,
)

train()

No per-iteration trigger is required. The integration observes framework optimizer boundaries and arms incidents at the arbitrary training iterations specified by the campaign. In a distributed job, every rank completes campaign preparation and capability validation before any current-iteration incident is armed. Preparation rejects target ranks outside the process-group world size and requires every rank to report the same canonical manifest and current_iteration. This prevents an iteration-one process, communication, or cluster fault from interrupting enablement consensus or running with divergent schedules. Later scheduled safe incidents use the same rank-consistent preflight and arming consensus at their optimizer boundary. Iterations without a scheduled campaign candidate add no distributed synchronization. The optional rank= override is reserved for non-distributed testing; in an initialized distributed job it must match the process's global rank. Overrides must be non-boolean integers; coercible strings and fractional values are rejected. Framework integration wraps the optimizer boundary itself rather than relying on a success-only post-step hook. When an optimizer attempt fails on one rank, every rank that attempted that scheduled boundary participates in failure consensus before cleanup; the failed attempt does not advance the campaign clock. The failing rank then re-raises its original optimizer error.

The first arguments match the framework lifecycle:

Framework Enablement
PyTorch enable_fault_injection(model, optimizer, campaign=...)
TorchTitan enable_fault_injection(trainer, campaign=...)
Megatron Core enable_fault_injection(model_chunks, optimizer, campaign=...)
DeepSpeed enable_fault_injection(engine, campaign=...)

Framework selection is automatic and is not stored in the campaign.

Training Iterations

A training_iteration is one logical global training step that normally ends at an optimizer update. It may contain multiple microbatches, forward calls, backward calls, pipeline stages, or gradient-accumulation passes.

An incident at iteration 100 is armed after iteration 99 completes and applies to the training work that produces optimizer update 100. Forward-call counts and SCOUT check cadence do not affect this clock.

The default clock uses absolute training-run iterations:

{
  "type": "training_iteration",
  "origin": "training_run"
}

TorchTitan and DeepSpeed progress is discovered from their training objects. Pass completed_iterations= when native PyTorch or Megatron is enabled after an externally managed resume. The value must be a non-negative integer; booleans, strings, and fractional values are rejected rather than coerced. Megatron optimizers commonly return an update_successful boolean as the first tuple element. A false value represents an overflow or otherwise skipped optimizer update and does not advance training_iteration. Use "origin": "campaign_start" when iteration numbers should be relative to enablement instead.

Incident Manifest

The versioned JSON format separates the incident schedule from its correlated failure effects:

{
  "schema_version": 1,
  "name": "mixed-production-failures",
  "seed": 17,
  "clock": {
    "type": "training_iteration",
    "origin": "training_run"
  },
  "incidents": [
    {
      "incident_id": "sporadic-sdc",
      "trigger": {
        "at": [37, 203, 911],
        "probability": 1.0
      },
      "lifetime": {
        "matching_calls": 1
      },
      "retrigger": "once",
      "faults": [
        {
          "fault_id": "hidden-output",
          "type": "tensor_corruption",
          "target": {
            "rank": 3,
            "component": "transformer_block",
            "index": 12,
            "surface": "output"
          },
          "parameters": {
            "operation": "sign_flip",
            "scope": "1%"
          }
        }
      ]
    },
    {
      "incident_id": "intermittent-straggler",
      "trigger": {
        "range": {
          "start": 300,
          "end": 500,
          "every": 20
        },
        "probability": 0.25
      },
      "lifetime": {
        "iterations": 3
      },
      "retrigger": "once",
      "faults": [
        {
          "fault_id": "collective-delay",
          "type": "delay",
          "target": {
            "rank": 5,
            "surface": "collective",
            "operation": "all_reduce"
          },
          "parameters": {
            "delay_ms": 500
          }
        }
      ]
    }
  ],
  "metadata": {
    "workload": "llama-pretraining",
    "topology": "dp=8,tp=2"
  }
}

Load and write manifests with FaultCampaign.from_json(...) and FaultCampaign.to_json(...). The versioned parser rejects unknown fields at every executable schema level, including misspelled trigger fields. Arbitrary user data remains allowed only inside metadata and executor-defined parameters.

Campaign Field Reference

A campaign has four nested levels:

campaign
  incidents[]
    faults[]
      target
      parameters

One campaign contains scheduled incidents. An incident may contain multiple correlated fault actions, and every action has its own target and executor parameters.

Campaign

Field Required Default Meaning
schema_version No 1 Manifest compatibility version. Serialized campaigns always include it; unsupported versions are rejected.
name Yes - Non-empty campaign identifier included in reports and state-store records.
seed No 0 Signed 128-bit seed used for deterministic probability selection and fault randomness.
clock No {"type": "training_iteration", "origin": "training_run"} Defines how incident trigger positions are interpreted.
incidents Yes - Non-empty array of incident objects. incident_id values must be unique in the campaign.
metadata No {} User-defined experiment metadata such as model, workload, topology, or ticket ID. It does not change execution.

Integer fields use JSON integers only. Booleans and fractional numbers are not coerced into iteration, rank, index, lifetime, retry, seed, or schema values. The campaign name, every incident_id, and every fault_id must be non-empty strings. Nulls, booleans, and numeric identifiers are rejected rather than converted to strings because identifiers participate in manifest hashes, probability selection, occurrence IDs, and persistent journal keys. Campaign metadata, target metadata, and fault parameters are validated as finite JSON values and captured as deeply immutable snapshots. Mutating the caller's source dictionaries after construction cannot change scheduling or the manifest identity.

Clock

Field Values Meaning
type training_iteration Schedule incidents at logical training iterations, not forward calls or microbatches.
origin training_run, campaign_start Use absolute training-run progress or count relative to campaign enablement.

With training_run, an incident at iteration 100 still refers to iteration 100 after a restart. With campaign_start, iteration 1 is the first iteration after enable_fault_injection().

Incident

Field Required Default Meaning
incident_id Yes - Non-empty identifier unique within the campaign.
trigger Yes - Candidate training iterations and optional selection probability.
lifetime Yes - How long each selected occurrence remains active.
retrigger No once Whether the same candidate may fire again after rollback or restart.
max_occurrences Conditional - Positive retry limit; valid only when retrigger is max_occurrences.
faults Yes - Non-empty array of correlated fault actions. fault_id values must be unique within the incident.

All actions in faults share the incident trigger, lifetime, occurrence ID, and temporal classification. For example, one incident can delay three ranks at the same iteration. That is one occurrence containing three fault actions.

The runtime generates an occurrence ID as <incident_id>@<iteration>. A repeated attempt is suffixed with #<attempt>, for example network-delay@500#2.

Trigger

Use exactly one of at or range.

Field Required Default Meaning
at One of at/range - Sorted, unique array of positive iteration numbers. Exact candidates use binary lookup, so healthy-step work remains bounded for large lists.
range.start For range - First candidate iteration; must be positive.
range.end For range - Last candidate iteration, inclusive. Range schedules are evaluated lazily and do not allocate one entry per candidate. Campaign incidents are held in lazy heap cursors, so healthy iterations do not scan every incident.
range.every No 1 Positive spacing between range candidates.
probability No 1.0 Selection probability for each candidate, from 0.0 through 1.0.

Probability does not make a campaign irreproducible. Selection is derived from seed, incident_id, and the candidate iteration, so the same manifest selects the same occurrences.

Lifetime

Use exactly one lifetime field:

Field Value Meaning
matching_calls Positive integer Apply the fault to that many matching target operations, then remove it. Not valid for weight, bias, or optimizer_state; use iterations or until so state mutations remain active through backward and the optimizer boundary.
iterations Positive integer Keep the fault active for that many training iterations, including its trigger iteration.
until recovery Keep it active until FaultInjectionSession.notify_recovery().
until replacement Keep it active until FaultInjectionSession.notify_replacement().
until campaign_end Keep it active until the session closes.

An until lifetime is permanent and therefore requires a single trigger candidate. Multiple bounded candidates or probabilistic bounded candidates are classified as intermittent; one bounded candidate is transient. For an iterations lifetime, candidate spacing must be at least the configured duration. Exact or ranged candidates whose active windows would overlap are rejected when the manifest is constructed. Separate incidents that can overlap on the same resolved local target are also rejected during enablement. Probability-zero incidents are ignored; for other probabilistic incidents the validator conservatively treats a candidate as potentially selected. Large ranged schedules are checked without materializing every candidate. Correlated actions inside one incident must also resolve to distinct local targets. Selector aliases that identify the same parameter, gradient, or optimizer-state entry are rejected during enablement instead of failing at the scheduled activation boundary. For built-in local effects, a matching_calls lifetime greater than one may span optimizer iterations because framework call multiplicity is not portable. Such an incident must have one trigger candidate, and a later incident on the same resolved target is treated as potentially overlapping. Repeated matching_calls=1 candidates retain the established one-call contract for each scheduled iteration. The eight-GPU validation campaign accepts only matching_calls=1, even for non-state faults. Framework call multiplicity is not portable enough to infer a multi-call effect's expiration or its required post-fault optimizer iteration. Closing a session normally completes a verified campaign_end effect. Closing before a matching_calls or iterations lifetime finishes cancels that effect, so a partial-duration injection cannot be certified as successful. Error-path cleanup cancels every still-active effect, including campaign_end effects.

Retrigger Policy

Value Behavior
once Fire a candidate at most once. This is restart-stable when the campaign state store survives worker replacement.
every_attempt Fire whenever execution reaches the candidate again after rollback.
max_occurrences Fire at most max_occurrences times for the candidate.

Fault Action

Field Required Meaning
fault_id Yes Non-empty action identifier unique within its incident.
type Yes Canonical observable failure effect, such as tensor_corruption, delay, or process_termination.
target Yes Framework-neutral location where the effect is applied.
parameters No Type- and executor-specific settings. Defaults to {}.

The runtime generates an injection ID as <occurrence_id>/<fault_id>. Correlated actions therefore have different injection IDs but the same occurrence ID.

Target

Field Required Default Meaning
surface Yes - Training surface: input, output, weight, bias, gradient, optimizer_state, rng_state, sampler_state, data, checkpoint, compute, collective, process, resource, or config.
rank No 0 Global rank that executes the action.
model_part No 0 TorchTitan model-part or Megatron model-chunk index.
component For logical module targets - Non-empty logical component string such as transformer_block, embedding, output, or expert.
index For indexed components - Global layer or expert index.
module_path For explicit module targets - Non-empty exact path from the user model's named_modules(), such as model.layers.12.mlp. It is attempted before logical component resolution.
operation Executor-specific - Non-empty runtime operation such as all_reduce for a collective fault.
resource Executor-specific - Non-empty string identifying a GPU, NIC, worker, node, or other resource.
path Executor-specific - Non-empty checkpoint or storage path.
metadata No {} Additional selector data consumed by framework resolution or a custom executor.

Module surfaces (input, output, weight, bias, gradient, optimizer_state, and compute) require either module_path or component. Only layer and expert logical components accept index; non-indexed selectors such as embedding and output reject it. Supplying both is useful when the exact framework path refines a logical component for localization comparison. The explicit module must be the logical module itself or one of its descendants; contradictory selectors are rejected before injection. For pipeline-sharded Megatron, TorchTitan, or DeepSpeed models, logical transformer-layer indices are resolved through global module metadata such as layer_number or global_layer_index. The injector rejects ambiguous stage-local numbering instead of binding the same local suffix on every stage. Megatron's layer_number is interpreted as one-based. Other pipeline implementations using layer_number must set metadata.layer_number_base to 0 or 1, or expose an unambiguous global_layer_index or global_layer_idx.

Expert indices are global. An expert module may expose global_expert_index or global_expert_id. Otherwise the target or model must provide expert_parallel_rank and num_local_experts; the injector maps the local module suffix through that topology and rejects ambiguous local numbering. Logical embedding targets prefer an unambiguous token, word, or vocabulary embedding. If only multiple generic embedding paths are available, enablement fails and the campaign must specify an exact module_path.

Built-In Parameters

Parameter Applies to Default Meaning
operation tensor_corruption Required single_bitflip, multi_bitflip, set_value, scale, noise, or sign_flip.
scope Local tensor and state-flow faults except reorder single Elements selected by the effect: single, row, 1%, 10%, or 100%. reorder accepts no scope and always reorders the full leading dimension.
magnitude Bit flip, scale, or noise corruption medium catastrophic, large, medium, subtle, or near_invisible; selects bit position or the default scale/noise strength.
value set_value Required Numeric value, or nan, inf, or -inf.
factor scale Derived from magnitude Explicit multiplication factor.
std noise Derived from magnitude Gaussian-noise standard deviation.
parameter Parameter, gradient, or optimizer-state target Surface-dependent Exact parameter attribute, commonly weight or bias.
state_key optimizer_state First suitable tensor Exact optimizer-state entry, such as exp_avg or exp_avg_sq.
delay_ms delay Required Finite positive JSON number in milliseconds that fits the platform timer. Strings, booleans, NaN, infinity, and unrepresentable durations are rejected.

Built-in local parameters are validated when the manifest is constructed. Unknown keys and keys that do not apply to the selected operation are rejected; for example, factor is valid only for scale and std only for noise. factor and std must be finite numbers, std must be positive, set_value accepts only a number or nan/inf sentinel, and state-flow scope values must be one of the documented selectors. Invalid values never survive until a model hook executes. A finite set_value must also fit the dtype observed at runtime; otherwise the action is recorded as failed and the hook returns the unmodified tensor. Built-in hook transforms also require a strided layout. Sparse or otherwise non-strided tensors are returned unchanged and recorded as unsupported instead of raising from one distributed rank. History observers apply the same rule: an unsupported sparse gradient or module value is remembered as unavailable history, and the later stale/duplicate occurrence fails locally without raising from the observer hook. Empty local tensors are also unsupported. A rejected observation invalidates that history slot, so a later dense value cannot reuse evidence from before the unsupported iteration. This includes empty module inputs and outputs; history selection does not silently skip them in favor of older evidence. Weight and bias collision detection is keyed by the resolved parameter storage, not by the selector spelling. A weight surface with parameter: "bias" and a direct bias surface therefore cannot overlap on the same tensor. Parameter-state selectors must name registered model parameters. Registered buffers such as BatchNorm running statistics are rejected because checkpoint replacement tracking and optimizer ownership apply only to parameters. Gradient targets must resolve to a parameter with requires_grad=True; otherwise enablement fails before the training loop installs a hook.

Stale and duplicate faults retain two observed values only during their scheduled collection window. Module and gradient observation starts one training iteration before a candidate and is removed at the optimizer boundary after the occurrence. Input, output, gradient, and state histories retain only the values selected by scope, rather than cloning the complete tensor for a narrow fault. A stale or duplicate incident scheduled for the first iteration fails verification because no prior value exists. For model and optimizer state, that missing history produces a failed ground-truth record during activation; it does not abort campaign enablement. Unsupported targets and no-op mutations remain hard validation failures. Optimizer entries such as Adam moments may not exist until the first optimizer step. History setup defers those unresolved entries and retries at later optimizer boundaries instead of rejecting campaign enablement prematurely. Megatron mixed-precision and distributed optimizers key those entries by FP32 main parameters rather than the model FP16/BF16 parameters. The framework adapter resolves the corresponding main parameter, including members of a chained optimizer, before selecting optimizer state. For state surfaces, an incident at iteration N is armed after iteration N-1 completes. It applies the snapshot from before iteration N-1, making the target one completed optimizer update behind its current state. The latest snapshot is the current state at arming time and would therefore be a no-op.

Weight, bias, and optimizer-state faults with bounded iteration lifetimes are retired by removing the injected finite delta from the current tensor. This preserves optimizer updates made during the active window. A bounded state fault whose injected delta is non-finite is rejected before mutation; represent such corruption with an until lifetime and recover the workload afterward. If the target still equals the injected value at retirement, the executor copies the original selected values exactly instead of subtracting a rounded low-precision delta. Delta subtraction is reserved for values changed by training while the effect was active. Megatron optimizers may replace a model FP16/BF16 parameter from its distinct FP32 main parameter after the update. That resynchronized value is preserved at retirement instead of having the injected delta subtracted a second time. DeepSpeed ZeRO-3 applies the same rule to a rank-owned ds_tensor partition that is replaced from its FP32 master partition during the optimizer step. FSDP2/HSDP DTensor state is mutated and verified through the rank-local shard. Model load_state_dict() marks only active weight and bias state as externally replaced; optimizer load_state_dict() marks only optimizer-state effects. This prevents an unrelated partial checkpoint load from suppressing cleanup of another state family. If a model load copies an active target and then fails on another key, the copied target is still marked as replaced before the original load error is re-raised. Recovery cleanup therefore preserves the partially restored value instead of subtracting an obsolete injection delta. Megatron and DeepSpeed GEMINI recovery copy checkpoint tensors directly rather than calling framework load_state_dict() methods. Those successful direct loads notify the same replacement tracker before notify_recovery() retires active state faults, so a recovered value equal to the injected value is not mistaken for a still-live fault and overwritten with pre-fault state. A bounded state effect replaced before its configured expiration is recorded as failed because the requested lifetime was not executed. Replacement at the normal post-update expiration boundary remains successful and preserves the restored state. Replacement is compared with the last completed optimizer iteration, so loading clean state after activation but before that iteration's optimizer boundary fails even a one-iteration occurrence.

Custom executors receive the full target and parameters objects unchanged. They may define additional fields, but should validate those fields before the training loop begins. All string selectors are validated before training and captured in the campaign's immutable manifest snapshot. Non-string or whitespace-only module_path, operation, and path values are rejected instead of being coerced by a framework adapter or executor.

Schedule Examples

Exact arbitrary iterations:

{"at": [37, 203, 911], "probability": 1.0}

Inclusive periodic range:

{
  "range": {"start": 100, "end": 500, "every": 20},
  "probability": 0.25
}

Permanent fault beginning at one exact iteration:

{
  "trigger": {"at": [1200]},
  "lifetime": {"until": "replacement"}
}

Failure Model

The canonical types cover common observable LLM pre-training failures:

Family Types
Numerical and state tensor_corruption, stale_state, drop, duplicate, reorder, config_drift
Performance and liveness delay, hang, timeout
Runtime and availability exception, resource_exhaustion, process_termination, resource_unavailable
Checkpoint and storage checkpoint_corruption, checkpoint_truncation, checkpoint_missing, io_error
Communication payload_corruption, collective_desync, message_drop, network_partition

The built-in local executor supports:

  • numerical corruption of module inputs, outputs, weights, biases, gradients, and optimizer-state tensors;
  • stale or duplicated inputs, outputs, parameters, gradients, and optimizer state;
  • dropped or reordered inputs, outputs, and gradients; and
  • delays on logical module or compute targets.

Tensor corruption operations are single_bitflip, multi_bitflip, set_value, scale, noise, and sign_flip. Scopes are single, row, 1%, 10%, and 100%. Bit flips support float16, bfloat16, float32, and float64.

Logical Targets

Prefer framework-neutral targets:

{
  "rank": 3,
  "component": "transformer_block",
  "index": 12,
  "surface": "gradient"
}

The built-in resolver recognizes transformer blocks, embeddings, output heads, and experts. Use module_path as an explicit escape hatch:

{
  "rank": 3,
  "module_path": "model.layers.12.mlp",
  "surface": "output"
}

Other target fields identify an operation, resource, or storage path. The execution rank applies the action and records rank-local ground truth. For a resource-only action, that execution rank is not automatically an expected failed rank. Localization should report the resource through failed_resources and include a failed rank only when the campaign also targets that rank directly. FaultInjectionRecord.expected_rank likewise returns None when the target has a resource but no explicit rank. When a target explicitly supplies both rank and resource, localization must report both dimensions; neither the rank-only nor resource-only result is complete attribution. The standalone example comparator applies the same rule and never substitutes an action's executor rank for a resource-only target.

Destructive and Environment-Specific Failures

Process, checkpoint, storage, communication, node, and network effects require an executor with explicit capabilities:

from lm_resiliency import (
    CallbackFaultExecutor,
    FailureType,
    FaultExecutionResult,
    SafetyClass,
)

executor = CallbackFaultExecutor(
    name="cluster-manager",
    supported_types={
        FailureType.PROCESS_TERMINATION,
        FailureType.RESOURCE_UNAVAILABLE,
        FailureType.NETWORK_PARTITION,
    },
    max_safety=SafetyClass.CLUSTER_DESTRUCTIVE,
    activate=lambda request: FaultExecutionResult(
        verified=manager.apply_fault(request.to_dict()),
        active=True,
        token=request.occurrence_id,
    ),
    deactivate=lambda request, result: manager.clear_fault(result.token),
)

faults = enable_fault_injection(
    model,
    optimizer,
    campaign=campaign,
    executors=(executor,),
)

Safety classes are:

  • safe_in_process;
  • isolated_destructive; and
  • cluster_destructive.

Campaign enablement fails before training when no configured executor supports a local fault or when an executor's safety ceiling is insufficient. Collective desynchronization expects SCOUT hang localization. drop, duplicate, and reorder actions on the collective surface are cluster-destructive and also expect hang, so they cannot use a safe-in-process executor or participate in an in-band post-activation collective. The same operations on tensor and data-flow surfaces retain their normal local semantics. Unsupported effects are never silently skipped. At execution time, the selected executor must return verified evidence. An unverifiable activation is marked failed, deactivated when necessary, and is not accepted as campaign ground truth. The executor result's verified and active fields must be JSON booleans; truthy strings and integers are rejected. Activation and deactivation evidence must be strictly JSON-serializable; invalid evidence fails the action immediately and active effects are deactivated. If deactivation is interrupted, the record is failed before the interrupt is re-raised and the session retains cleanup ownership until a retry succeeds. An executor that returns an active effect must provide a deactivation callback. A callback executor without deactivation must declare one_shot=True, which is an explicit promise that activation returns active=false. This capability is validated before the callback runs; returning an active result from a one-shot executor is still rejected. An external executor used with a matching_calls lifetime must declare completes_inline=True and return active=false. This capability is checked before activation so an unsupported destructive effect is never started. For an iterations lifetime greater than one, or any until lifetime, the executor must return active=true and support deactivation. An inline, inactive result cannot prove that a persistent effect remained present for its configured lifetime. Executors declared one_shot=True are therefore rejected for those lifetimes during preflight. Executor activation and deactivation evidence is captured as a deep JSON snapshot, so later mutation of callback-owned dictionaries or lists cannot change campaign ground truth.

Distributed preflight and attempt persistence complete on every rank before any executor runs. After activation begins, an in-band post-activation consensus is used only when every selected action is safe_in_process. A successful process termination, hang, resource loss, or network partition may make its execution rank unable to enter another training-process collective, so destructive executors must report activation and failures through their training manager or other out-of-band control plane. Their later expiration also avoids in-band consensus for the same reason. If any runtime preparation, preflight, persistence, rollback, or safe-activation collective itself raises, the surviving rank performs bounded local cleanup before propagating the collective failure. This includes interrupt-class failures raised by the collective operation itself: active effects and framework callbacks are cleaned before the original interrupt is re-raised. An interrupt-class failure raised during safe activation participates in that same all-rank failure consensus. Newly armed effects are rolled back on every rank before the interrupted rank re-raises its original exception. If one correlated effect's rollback raises an interrupt-class error, rollback continues through every earlier effect. The original activation failure remains primary and the first cleanup failure is attached as secondary context. Single-rank and intentionally non-consensus preparation failures follow the same cleanup rule.

Built-in delays use an interruptible wait. Recovery or session cleanup signals that wait before acquiring the effect lock, so shutdown does not wait for the configured delay duration.

The kit injects observable effects, not physical defects. For example, an ECC event is represented as tensor corruption or resource loss; a failed cable is represented as delay, message loss, or network partition.

Restart and Retrigger Behavior

Each candidate occurrence is preflighted and then journaled before activation. This ordering applies to both distributed and single-rank future iterations, so an invalid target or executor does not consume a retry attempt. If a later single-rank preflight fails, the session closes and restores every effect already active from an earlier iteration before propagating the error. Distributed journal-binding consensus failures follow the same rule: the deferred session removes its callbacks before the collective exception escapes. The default retrigger: "once" prevents the same occurrence from firing again after rollback when a persistent state store is used.

from lm_resiliency import JsonCampaignStateStore

faults = enable_fault_injection(
    model,
    optimizer,
    campaign=campaign,
    state_store=JsonCampaignStateStore(f"/manager-state/campaign-rank-{rank}.json"),
)

Supported policies are:

  • once;
  • every_attempt; and
  • max_occurrences, with a positive max_occurrences field.

The state file must survive worker replacement for restart-stable behavior. Use one JSON state file per rank, or provide a manager-backed CampaignStateStore implementing atomic compare_and_swap(expected, updated) claims across workers. A plain load-modify-save sequence is insufficient when old and replacement workers overlap: only one worker may claim a retrigger: "once" occurrence. JsonCampaignStateStore uses an interprocess file lock, atomic replacement, and a parent-directory fsync before acknowledging a claim; MemoryCampaignStateStore provides the same contract within one process. The exact state_store object passed by the caller is retained even when the object defines falsey truth-value semantics; only None selects the default in-memory store. Both journals passed to compare_and_swap must name the same campaign; cross-campaign updates are rejected before storage is modified. For a distributed enablement, the initial manifest binding is persisted only after every rank agrees on the manifest, training iteration, and persisted attempt journal. Divergent per-rank attempt histories fail enablement before any new occurrence is armed. Interrupt-class preparation failures participate in the same rank consensus before the original interrupt is re-raised. Cleanup preserves the first interrupt or error while continuing through every active effect, observer, and framework restoration. The same rule applies at later optimizer boundaries: interrupt-class preflight, attempt-persistence, and safe-effect expiration failures enter rank consensus before the interrupted rank re-raises the original signal. If an interrupt arrives after a built-in local mutation or hook registration begins, the partially constructed effect restores or removes that state before the interrupt escapes. Recovery and replacement notifications attempt every matching effect cleanup before returning. An ordinary cleanup failure is reported with its lifecycle boundary; an interrupt-class failure is re-raised unchanged after the remaining effects have been finalized and discarded. The journal stores the canonical manifest identity. Reusing a campaign name with changed triggers, targets, parameters, or metadata requires a new state file; stale attempts are never applied to an edited manifest. Attempt entries use canonical <incident_id>@<positive_iteration> keys and strictly positive integer counts. Iterations have no leading zeroes. Malformed, boolean, fractional, zero, or negative restart evidence is rejected rather than normalized. Deterministic probability skips are reported as skipped_probability but are not persisted as attempts. Reaching a dense range with unselected candidates therefore does not grow or repeatedly copy the restart journal. Artifact evaluation recomputes selection from the authenticated manifest seed, incident ID, and iteration. A skipped occurrence must include every manifest action, and a selected occurrence cannot claim a skipped status. The journal object requires exactly campaign, manifest_identity, and attempts; missing or unknown fields are rejected.

Ground Truth and Evaluation

An occurrence ID identifies one incident candidate, such as sporadic-sdc@203. Correlated faults share the same occurrence ID and have individual injection IDs.

from lm_resiliency import LocalizationResult

report = faults.evaluate(
    [
        LocalizationResult(
            occurrence_id="sporadic-sdc@203",
            detected=True,
            failed_ranks=(3,),
            kind="sdc",
            components=("transformer_block",),
            latency_ms=8.4,
        )
    ]
)
report.to_json("campaign-report.json")

A correlated occurrence may submit multiple LocalizationResult objects with the same occurrence_id. This is required when one incident contains different failure kinds or when the resiliency system emits separate reports for its actions. Each expected kind must correlate with its injected rank or resource; matching only the aggregate sets is insufficient. When a kind contains actions that specify both rank and resource, the exact (rank, resource) pairs must also match within that kind. Correct rank and resource projections with the pairs swapped are not successful attribution. Neutral localization results may include scope. A peer_group result counts as detection evidence but is inconclusive for rank or resource localization, so its targets, kind, and components receive no attribution credit.

Reports contain:

  • the canonical manifest identity;
  • the complete versioned manifest;
  • actual iteration and attempt numbers;
  • temporal behavior and safety class;
  • executor and verification evidence;
  • expected ranks, resources, and components;
  • submitted neutral localization results; and
  • attribution accuracy and latency.

An occurrence is localized only when injection succeeded, detection was reported, and the reported rank and resource sets exactly match the expected sets. Extra targets are attribution errors, not successful localization. If a localization result also supplies kind or components, that evidence must match the injected fault; omit optional evidence when the resiliency system does not report it. When an action explicitly identifies both a rank and a resource, localization must preserve that rank-resource association. Correct aggregate rank and resource sets with the pairs swapped are not successful attribution. The standalone comparator applies the same association per failure kind. Positive SCOUT layer_id evidence is also associated with the failed ranks and resources in the same report, including exact rank-resource pairs. Swapping correct pair sets between layers is not successful localization even when each layer has the correct independent rank and resource projections. For embedding, output-head, resource, and other non-layer targets, any positive layer_id is contradictory evidence and fails localization; an omitted layer or the aggregate -1 sentinel remains valid. kind, when supplied, must be a non-empty string. Supplying component evidence for an occurrence whose injected targets have no expected component is an overclaim and fails attribution. failed_resources and components must be arrays of non-empty strings; scalar strings and coercible non-string values are rejected. When component evidence is supplied, it must exactly match the expected component set; extra components are attribution errors. Component evidence is also correlated with the exact (rank, resource) target association in each localization result. Swapping correct component names between two target pairs is therefore an attribution failure even when every component has the same independent rank and resource projections. When kind evidence is present, the full (kind, component, rank, resource) association must match. When kind evidence is also supplied, the (kind, component) association must match the injected actions; independently correct kind and component sets cannot be swapped across a correlated occurrence. The standalone SCOUT comparator applies the same rule to replay source prefixes: each hidden.*, embedding.*, or output.* source remains associated with the exact (rank, resource) pairs in the report that supplied it. Correct per-prefix rank and resource projections with their pairs crossed do not count as localization. Repeated actions with the same source and target are deduplicated for this set comparison. Parameter and optimizer-state SDC actions require both their hidden.* component evidence and SCOUT's optimizer.* updated-weight evidence; both families retain the same expected target association. Example detection counts also require a report with the expected failure kind; an unrelated report at the same iteration is retained as evidence but does not count as detecting the injected occurrence. For correlated incidents containing multiple failure kinds, each kind must identify its corresponding expected ranks; matching only the aggregate rank and kind sets is insufficient. SCOUT reports with scope="peer_group" are inconclusive. They may count as detection evidence, but their ranks, resources, kind, layer, and source fields are excluded from localization credit. evaluate() takes the campaign lifecycle lock before snapshotting every record, so expiration, recovery, replacement, and close cannot expose a half-transitioned correlated incident. A returned CampaignReport therefore represents one coherent lifecycle instant. Direct FaultInjectionRecord.to_dict() serialization uses the record lock and cannot expose a partially updated individual transition. Optimizer-boundary callbacks, recovery/replacement notifications, and session cleanup share one lifecycle lock. close() waits for an in-flight boundary and removes any effect or observer armed there before marking the session closed. External effect completion and rollback are independently serialized so a manager callback is never deactivated twice.

Reports are rank-local. A distributed campaign runner or training manager should collect reports from all ranks and preserve the software, hardware, topology, model, and workload metadata. A rank-local evaluation of a correlated multi-rank occurrence remains unsuccessful when records for any manifest action are absent, even if the submitted localization identifies every job-wide target. Aggregate the rank-local injection records, as the eight-GPU example does, before certifying the complete occurrence. The example comparator also checks the embedded manifest and requires every fault action for an occurrence exactly once; a partial or duplicate aggregate cannot pass. It additionally requires a record for every manifest candidate at or before the artifact's completed_iterations, including explicit probability skips. Losing an entire occurrence therefore cannot reduce the certification denominator. The example treats the embedded manifest as the authenticated source of expected fault type, target, parameters, rank, resource, and component evidence. Each injection record must match that manifest exactly. Embedded incident_id values must be unique; duplicate IDs are rejected rather than allowing a later incident to replace an earlier action map. injection_succeeded and verified must be JSON booleans and must agree with the record lifecycle status; truthy strings, inconsistent statuses, and record-level target tampering are rejected before localization scoring.

Current Boundaries

The schema and executor contract cover all canonical families above. The repository does not directly terminate the current process, damage arbitrary storage, alter a production network, or remove cluster resources. Those effects require an explicitly configured isolated or cluster executor.

DeepSpeed clocks advance only when global_steps advances, so gradient accumulation microbatches do not consume campaign iterations. PipelineEngine uses the active OptimizerStep instruction-map entry, composing with an already enabled SCOUT wrapper. Under ZeRO-3, weight and bias effects mutate the rank-owned ds_tensor partition rather than the zero-sized model placeholder; gradient hooks remain attached to the original parameter so they receive the materialized backward gradient. Optimizer-state effects use the rank-owned FP32 master-state fragment exposed by ZeRO-1/2 high-precision mappings or the ZeRO-3 local partition. ZeRO-3 optimizer offload is rejected because the injector cannot safely retain a live swapped state fragment across the fault lifetime.

Permanent non-finite mutation of live parameter or optimizer state requires checkpoint recovery because the affected values cannot be retired while preserving subsequent updates. Use those state faults in isolated campaigns that recover or terminate after localization. notify_recovery() preserves values that were replaced by checkpoint loading and only removes an effect that is still visibly present.

Before mutating or restoring live CUDA parameter or optimizer-state storage, the built-in executor synchronizes pending work on that device. This prevents a rare injected write from racing an asynchronous checkpoint copy that is still reading the same storage. The synchronization is limited to state-fault activation and cleanup; healthy optimizer iterations remain asynchronous.