Statistik
Der Bootstrap, der sich nicht reproduzieren ließ
Zwei Läufe derselben Analyse — gleiche Daten, gleicher Workflow, gleicher Seed — liefern zwei verschiedene Konfidenzintervalle. Nicht dramatisch verschieden: [12.31, 13.02] gegen [12.32, 13.01]. Ein Zittern in der zweiten Nachkommastelle; die Sorte Abweichung, die ein müder Mensch unter „Bootstrap halt“ ablegt und weitergeht. Tu es nicht. Das hier ist der Weg, der aus „flaky“ eine Diagnose macht — und der Schuldige sitzt, wie so oft, in Code, den du selbst geschrieben hast.
Ein durchgerechnetes Beispiel
Ein konstruierter Fall, kein Laborbericht: Das Studio ist noch nicht geöffnet (wo wir stehen). Die Statistik unten ist exakt, und du kannst jede Zeile davon auf Papier nachrechnen. Das Verhalten der Plattform — Determinismus-Stufen, der zählerbasierte RNG, Content-Hashes an jedem Knoten — ist das, wofür die Engine gebaut und verifiziert ist. Das Run-Log zeigt die Form des Belegs, nicht eine Session, die jemand hatte.
Zwei Intervalle, ein Seed#
Die Ausgangslage ist Alltagsstatistik. Eine rechtsschiefe, positive Messgröße: 431 Zeilen aus einer vorgelagerten Verkettung, die 19 davon doppelt geliefert hat; nach der Deduplizierung also 412. Bei dieser Schiefe wäre Mittelwert ± Standardfehler eine unehrliche Zusammenfassung, der Schätzgegenstand ist deshalb der Median: 12.67, das Mittel der 206. und der 207. Ordnungsstatistik. Für das Intervall das naheliegende nichtparametrische Werkzeug: der Bootstrap. Ziehe 412 Zeilen mit Zurücklegen aus den 412, nimm den Median, wiederhole das B = 10 000-mal; jedes Resample lässt im Mittel etwa 36.8 % der Zeilen aus — das e⁻¹ der Folklore — und die Streuung der zehntausend Mediane schätzt das Stichprobenrauschen deines einen. Das 95-%-Perzentilintervall liest die empirischen 2.5- und 97.5-Perzentile ab: der Sache nach die 250. und die 9 751. der sortierten Bootstrap-Mediane. Welche Quantilkonvention genau, deklariert der Quantil-Op — wer je das „type 7“ der einen Bibliothek mit dem Default einer anderen versöhnen musste, weiß, dass diese Fußnote zweite Nachkommastellen entscheidet. Es gibt Feineres, etwa BCa; für einen schlichten Median bei n = 412 ist das Perzentilintervall vertretbar.
Also: Seed 42, B = 10 000, [12.31, 13.02]. Lass den Workflow vor dem Commit noch einmal laufen — aus Ordnungsliebe, nicht aus Misstrauen. Gleicher Seed. [12.32, 13.01].
Der statistische Reflex#
Der ehrliche erste Instinkt ist: quantifizieren. Die Endpunkte eines Perzentilintervalls sind selbst Monte-Carlo-Schätzungen, mit Standardfehler √(α(1−α)/B), geteilt durch die Bootstrap-Dichte am jeweiligen Quantil. Haben die zehntausend Mediane eine Standardabweichung von rund 0.18, liegt unter Normalapproximation die Dichte nahe dem 2.5-%-Quantil bei etwa 0.058/0.18 ≈ 0.32 und der Monte-Carlo-Standardfehler jedes Endpunkts bei ungefähr 0.005. Beide beobachteten Verschiebungen liegen bei etwa 0.01 — zwei Monte-Carlo-Standardfehler. Zwischen Läufen mit verschiedenen Seeds ist das Schulterzucken-Territorium; wen es stört, der vervierfacht B und halbiert den Fehler.
Aber die Seeds sind nicht verschieden. Unter fixem Seed ist die Referenzverteilung der Differenz nicht „klein“ — sie ist eine Punktmasse bei null. Die Größe der Abweichung trägt keine Information; ihre Existenz ist der ganze Befund. Und harmlos wirkt alles wegen des anderen Indizes: Der Punktschätzer reproduziert exakt, 12.67 in beiden Läufen — einem Median über die volle Stichprobe ist die Zeilenreihenfolge gleichgültig. Gesund sieht alles aus bis auf die Ränder des Intervalls, und genau dort erwartet ein müder Mensch das Rauschen.
Drei Verdächtige#
Verdächtiger eins: die GPU. WebGPU-Float-Ausführung ist nicht assoziativ — die Reihenfolge einer Reduktion ist über Schedules hinweg nicht garantiert —, und genau deshalb verspricht die prod-Stufe Reproduktion innerhalb einer deklarierten Toleranzklasse statt Bit-Gleichheit. Aber ein Bootstrap-Index ist eine ganze Zahl; um „Zeile 227“ gibt es keine Toleranzkugel. Ein Median ist eine Auswahl — bei geradem n eine Addition und eine Halbierung —, keine lange Akkumulation. Und 0.01 auf 12.3 sind relativ etwa 8 × 10⁻⁴, Größenordnungen jenseits von allem, was ein Auswahl-Op mit ernster Miene deklarieren könnte. Plausibler Verdächtiger, schwaches Motiv.
Verdächtiger zwei: die Zufallszahlen der Plattform. Scellis-RNG ist zählerbasiert, Philox-artig: Ein Zug ist eine pure Funktion von (Seed, Stream, Zähler) — kein versteckter globaler Zustand, gleiche Koordinaten, gleiche Ausgabe auf jedem Backend, mit benannten Sub-Streams für Shuffles, Dropout, Augmentierung, und der Zustand wird mit dem Lauf gecheckpointet. Auf Unschuld gebaut — aber die Doktrin dieser Plattform ist, dass Unschuld verifiziert wird, nicht behauptet. Verdächtiger drei: dein eigener Vorverarbeitungs-Block.
Das erste Instrument ist die Stufenleiter selbst. Die Determinismus-Stufe eines Laufs ist explizit — am Lauf verzeichnet, nie stillschweigend defaultet. Stell den Workflow von prod auf debug: bitweise identisches Replay bei gleichem Seed auf demselben Gerät, ehrlich bezahlt mit deterministischen Kernel-Varianten und fixierten Reduktionsreihenfolgen (langsamer, und der Vertrag sagt das dazu). Zwei debug-Läufe, zwei verschiedene Artefakt-Hashes. Das ist ein Urteil, kein Symptom. Auf debug ist „weicht überhaupt ab“ per Definition ein Defekt — keine Statistik nötig, kein Ermessen. Irgendwo fließt echte Entropie in den Lauf.
Das zweite Instrument ist der Referenzpfad. Jeder eingebaute numerische Op trägt eine CPU-Referenz — die, gegen die jeder GPU-Kernel geprüft wird. Lass den Workflow dort zweimal laufen: bit-exakt und von jeder GPU unabhängig. Immer noch zwei verschiedene Hashes. Das Silizium ist freigesprochen; was auch immer würfelt, wohnt im Programm, nicht in der Hardware.
Eingrenzen per Hash#
Hier hört die Plattform auf, Instrument zu sein, und wird zur Landkarte. Jedes Zwischenartefakt ist inhaltsadressiert, und die Provenienz eines Laufs verzeichnet den Hash an jedem Knoten. Leg die beiden debug-Läufe nebeneinander und lauf den Graphen ab: Ingest — identische Hashes. Typisierung, Zusammenfassungen — identisch. Der Deduplizierungs-Block — verschieden. Erste Divergenz; alles stromabwärts erbt das Gift. Der Knoten: user/kemal/dedup_rows@1 — deiner, an einem Nachmittag Wochen zuvor selbst gebaut.
// user/kemal/dedup_rows@1 — reference body (the guilty version)
const rows = dedupeByKey(input.rows, "sample_key");
// notebook reflex: "shuffle so later splits don't inherit file order"
for (let i = rows.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // <- unseeded entropy
[rows[i], rows[j]] = [rows[j], rows[i]];
}
return { rows };Die Deduplizierung ist die ehrliche Hälfte. Danach kommt ein Fisher–Yates-Shuffle über die Zeilen, angetrieben von Math.random() — ungeseedet, unbenannt, unsichtbare Entropie. Ein Notebook-Reflex: nach dem Dedup mischen, damit spätere Splits nicht die Dateireihenfolge erben. Du hattest vergessen, dass er da war. Die Metadaten des Blocks deklarieren unterdessen Determinismus — eine Deklaration, die dir die Plattform für einen gesandboxten Body auf deinem eigenen Gerät vorläufig glaubt, und sie ist ehrlich, was das angeht. Statische Analyse überführt diese Sorte Lüge nicht; der Job des Vertrags ist ein anderer: Er macht die Lüge an einem einzigen Abend falsifizierbar.
Der Fix, der sich selbst löscht#
Das Lehrreichste ist, wofür der Shuffle da war: für nichts. Ein Bootstrap zieht Zeilen über gleichverteilte Zufallsindizes, und eine Permutation, komponiert mit gleichverteilten Zügen, bleibt gleichverteilte Züge. Die Intervalle beider Läufe waren gültige 95-%-Perzentilintervalle, gezogen aus exakt derselben Monte-Carlo-Verteilung. Nirgends ein Bias; an keiner der beiden Zahlen war etwas falsch.
Hinweis
Kein p-Wert findet diesen Bug. Der ungeseedete Shuffle hat keine Verteilung verändert — beide Intervalle waren gültig, der Schätzer blieb unberührt. Kaputt war allein die Reproduzierbarkeit, und die ist eine Eigenschaft von Bytes, nicht von Verteilungen. Entdecken kannst du so etwas mit einem Hash, nicht mit einem Test.
Version 2 des Blocks löscht den Shuffle also einfach — Keep-first-Deduplizierung über identischen Eingabe-Bytes ist von allein reihenfolgestabil. Wo ein Shuffle wirklich zählt, etwa bei einem Holdout-Split, ziehst du ihn aus dem geseedeten Stream des Laufs — einem benannten Sub-Stream des zählerbasierten RNG —, und er reproduziert und checkpointet wie alles andere. Version 1 verschwindet dabei nicht: Ihre Bytes sind per Hash in der Provenienz beider Tatort-Läufe verankert, zitierbar für immer. Du kannst deinen Fehler nicht still umschreiben, nur ablösen. Unveränderlichkeit schmeichelt niemandem — und es lohnt sich, mit ihr Frieden zu schließen.
tier debug · seed 42 · same device
run A -> ci_artifact sha256:6b01…e4
run B -> ci_artifact sha256:9d57…22 ✗ debug promises bitwise equality
after the fix — user/kemal/dedup_rows@2
run A′ -> ci_artifact sha256:c8a3…7f
run B′ -> ci_artifact sha256:c8a3…7f ✓ identical bytesDann der eigentliche Lauf: B = 10 000, Seed 42, Stufe debug — [12.31, 13.02]. Dieselben Ziffern wie beim ersten Lauf des Abends, und zum ersten Mal verdienen sie den bestimmten Artikel: das Intervall.
Was debug wirklich festnagelt#
Was aus diesem Weg bleibt, ist die Leiter, präzise formuliert. debug nagelt bitweise identisches Replay fest — gleicher Seed, gleiches Gerät — über deterministische Kernel, fixierte Reduktionsreihenfolgen und den zählerbasierten RNG, zu echten Durchsatzkosten. Bit-Gleichheit über Geräte hinweg verspricht es nicht: Floats auf verschiedenen GPUs dürfen legitim voneinander abweichen; Bit-Exaktheit über Geräte ist der Job des Referenzpfads, und prod verspricht stattdessen eine deklarierte Toleranzklasse. fast erlaubt Nichtdeterminismus nur dort, wo er als unbedenklich deklariert ist. Und keine Stufe wird je stillschweigend defaultet — sie ist in die Provenienz des Laufs gestempelt und hängt an dem Link, den du teilst.
Committe den Workflow und präge den Snapshot-Link:
scellis.com/studio?repro=c8a3…7f
dataset@1 · user/kemal/dedup_rows@2 · B 10000 · seed 42 · tier debugWer ihn öffnet, löst die committeten Bytes wieder auf — Daten, Block-Versionen, Seed, Stufe; „bit-für-bit“ meint genau diese Bytes. Wer auf dem Referenzpfad nachrechnet, reproduziert sie exakt; wer auf einer anderen GPU auf prod nachrechnet, reproduziert innerhalb der deklarierten Toleranz — und die Plattform sagt dir, welches von beidem du bekommst, statt mehr zu versprechen, als Floats halten können. Die Praxis steht Schritt für Schritt im Guide.
„Flaky“ ist keine Diagnose. Es ist das Geständnis, dass in deinem System Entropie steckt, die du nicht benannt hast. Die Statistik erzieht uns dazu, die Unsicherheit zu quantifizieren, die wir meinen. Das Mindeste, was wir von unseren Werkzeugen verlangen können: dass jede Unsicherheit, die sie hinzufügen, mit Namen, Stream und Seed kommt.