mixle.utils.parallel.sdc_audit module¶
Silent-data-corruption (SDC) audit for the resilient mp EM backend (K5).
Status audit this module leans on (see K4’s docstring in resilient_em.py, which this
module builds directly on top of): fits are deterministic given seed (#115) and sufficient
statistics are ADDITIVE. K4 already turns those two facts into exact, cheap failure
recovery (a crashed worker’s shard is just redone). K5’s contribution is different: it turns
the SAME two facts into a cheap corruption detector.
The idea. Silent data corruption (SDC) is a bit-flip in memory or compute that produces a
WRONG result with no exception and no crash – the fit just silently converges to a corrupted
answer. For gradient-based training this is expensive to catch (you’d have to redo an entire
gradient step, or use redundant hardware). mixle’s additive-stat EM makes it cheap: recompute
the SAME shard’s accumulator TWICE, from the SAME raw shard bytes / same estimator / same
model / same sub_chunks split, once on the shard’s normal (“primary”) rank and once on a
DIFFERENT (“audit”) rank.
The verdict is deliberately not based on pickle-byte equality. Healthy heterogeneous workers
can differ in serialization order or in the last numerical bits because of CPU/GPU kernels,
BLAS implementations, fused operations, or library versions. Payloads are decoded into a
closed typed statistic, compared under the explicitly configured audit_rtol/audit_atol
numeric envelope, and separately canonicalized for exact evidence digests. Every mismatch
receipt records that envelope, maximum observed errors, the typed canonical digests, and the
raw payload digests. Exact raw agreement remains useful evidence but is never assumed to be a
portable proof of worker health.
NaN/Inf watchdog. mixle.models._neural_serial.check_finite already guards individual
density evaluations. K5 extends that same “fail loud, immediately, with the offending
location named” philosophy to the accumulator-fold boundary: finite_guarded_fold() is a
drop-in replacement for checkpointed_fold() that
checks finiteness of the running accumulator’s value() immediately after EVERY
combine() call, not just once at the end – so a NaN/Inf introduced while folding payload
i is caught at payload i’s combine() boundary, before it silently propagates into
payload i+1..n. The fully tied value is checked again after key_merge/key_replace
so shared-key transformations cannot introduce an unchecked invalid state. Scope note: this
wraps operations made by THIS module’s own fold loop only – it deliberately does NOT touch
SequenceEncodableStatisticAccumulator.combine()’s contract itself, which every other
caller in the codebase still uses unguarded, exactly as before.
- class AuditedMPEncodedData(data, estimator=None, encoder=None, num_workers=None, sub_chunks=1, max_retries=2, audit_rate=0.1, audit_rtol=1.0e-12, audit_atol=1.0e-12, rng=None, worker_timeout_s=30.0)[source]
Bases:
ResilientMPEncodedDataResilientMPEncodedData(K4) plus a continuous SDC audit (K5).Every round (
pysp_seq_estimate/pysp_stream_accumulate), a randomaudit_ratefraction of shards are ALSO recomputed on a second, different rank via the same ad hoc"update_shard"wire command K4 already uses for shard recovery (seeResilientMPEncodedData._recover_shard) – no new worker-side machinery. The primary and audit(count, value())payloads are compared as typed statistics under an explicit numerical envelope. A mismatch:is recorded as a structured
SDCAuditReceipt(self.audit_receipts,self.last_round_audit_mismatches);obtains a third independent witness before identifying a suspect. A two-way disagreement alone is recorded but never used to quarantine either worker;
stages every affected shard on validated survivors before atomically committing the suspect’s retirement. If no safe witness or placement exists, quarantine is deferred and that decision is recorded in the receipt.
The main EM round itself (retry, blacklisting on repeated failure, checkpointed fold) is entirely K4’s, untouched, reused via inheritance – K5 only adds the audit phase (run BEFORE the main round each call, so a quarantine decided by the audit is already reflected in that same round’s live-worker set) and swaps K4’s plain
checkpointed_foldforfinite_guarded_fold()via the_fold_fnhook K4 exposes for exactly this purpose.- Parameters:
audit_rate (float) – fraction of shards (
0..1) redundantly recomputed each round.rng (np.random.RandomState | None) – drives which shards are audited each round and which live rank is picked as the second (“audit”) rank.
audit_atol (float) – finite nonnegative tolerances used for floating and complex statistic leaves. Integer and categorical leaves remain exact.
data (Sequence[Any])
estimator (Any | None)
encoder (Any | None)
num_workers (int | None)
sub_chunks (int)
max_retries (int)
audit_rtol (float)
audit_atol
worker_timeout_s (float)
- Testing hook:
arm_corruption()registers a one-shot-per-round hook that can mutate a payload right after it comes off the wire from an ad hoc"update_shard"recompute, letting a test inject a deterministic, reproducible corruption (seeinject_bit_flip()) into a chosen (rank, shard, role) combination without touching worker internals – the same “observe the wire, mutate deterministically” pattern K4’sarm_killuses.
- arm_corruption(hook)[source]
Register
hook(worker_id, shard_id, role, payload_bytes) -> payload_bytes(roleis"primary"or"audit"), applied to every ad hoc"update_shard"payload this instance receives until cleared. Unlikearm_killthis is NOT one-shot by default (an SDC fault is typically persistent, e.g. a stuck bit in one DIMM) – clear it explicitly witharm_corruption(None)to simulate a transient fault.- Parameters:
hook (Any)
- Return type:
None
- pysp_seq_estimate(estimator, prev_estimate)[source]
One distributed EM step, tolerant of a worker dying mid-accumulation.
- class SDCAuditReceipt(round, shard_id, primary_worker, audit_worker, primary_nbytes, audit_nbytes, first_diff_byte_offset, primary_sha256, audit_sha256, comparison_mode='typed_numeric_envelope/v1', audit_rtol=0.0, audit_atol=0.0, max_abs_error=0.0, max_rel_error=0.0, mismatch_reason='', primary_canonical_sha256='', audit_canonical_sha256='', witness_worker=None, suspected_workers=(), quarantine_status='not_requested', primary_value_repr='', audit_value_repr='')[source]
Bases:
objectA structured record of one detected primary-vs-audit mismatch: which shard, which two ranks, and a summary of what actually differed (never just a bare “mismatch” boolean).
- Parameters:
round (int)
shard_id (int)
primary_worker (int)
audit_worker (int)
primary_nbytes (int)
audit_nbytes (int)
first_diff_byte_offset (int | None)
primary_sha256 (str)
audit_sha256 (str)
comparison_mode (str)
audit_rtol (float)
audit_atol (float)
max_abs_error (float)
max_rel_error (float)
mismatch_reason (str)
primary_canonical_sha256 (str)
audit_canonical_sha256 (str)
witness_worker (int | None)
quarantine_status (str)
primary_value_repr (str)
audit_value_repr (str)
- finite_guarded_fold(estimator, payloads, where='AuditedMPEncodedData.combine')[source]
checkpointed_fold, but check finiteness of the running accumulator immediately after EVERYcombine()call (see module docstring for why this is a wrapper around this module’s own fold loop rather than a change tocombine()’s contract).
- inject_bit_flip(payload, bit_offset=None)[source]
Flip exactly one bit of
payload, deterministically, and return the corrupted bytes.A real, reproducible stand-in for a hardware/software bit-flip: this is the corruption primitive the acceptance tests inject via
AuditedMPEncodedData.arm_corruption().bit_offsetdefaults to the middle bit of the payload (an arbitrary but fixed choice – determinism of the test does not depend on which bit, only that the two payloads it is applied to differ afterward).