773 lines
35 KiB
C
773 lines
35 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_two_pass_parity_gate.c
|
|
* @brief The accelerator parity gate as TWO SEPARATE PASSES paired offline — same corpus,
|
|
* same criteria, same verdict as test_chat_parity_gate.c, without ever putting a
|
|
* 26-second CPU forward between two consecutive card forwards.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHY THE INTERLEAVED GATE HAD TO BE REPLACED
|
|
* ---------------------------------------------------------------------------
|
|
* MEASURED ON SILICON 2026-08-18. The interleaved gate runs the accelerator and the CPU
|
|
* oracle alternately, position by position. On the TT host the oracle costs **~26 s a scored
|
|
* position** — 4.5 GB of major page faults a token, because a 9.5 GB Q8_0 mmap does not fit a
|
|
* 7.3 GB host — so every card forward is followed by 26 s of silence on the card. A resident
|
|
* queue is BOUNDED on purpose (`idle_spin_limit`), so the triads retire while they wait. The
|
|
* gate logged **802 / 2406 / 442 descriptor refusals** across its arms against **0** on
|
|
* single-arm runs of the same ops at the same length, and the GDN arm died at position 1 of
|
|
* document 0. The recovery is real and the results reproduce; a harness that provokes 2406
|
|
* refusals is still not one to gate an arm on.
|
|
*
|
|
* It is also 3x the work it needs to be. The oracle pass is IDENTICAL for every arm scored
|
|
* against it, and interleaved every arm pays for its own copy — the Phase 6 ladder was ~14 h
|
|
* for what is one ~3 h oracle pass plus minutes of card passes.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* THE THREE MODES
|
|
* ---------------------------------------------------------------------------
|
|
* --arm FILE run the ACCELERATOR only, over the whole corpus, dumping every scored
|
|
* position's fp32 logit row. Nothing else runs in this process, so the
|
|
* residencies are fed at decode cadence exactly as a serve feeds them.
|
|
* --oracle FILE run the CPU ORACLE only, the same way. This is the ~3 h pass and it is
|
|
* CACHEABLE: score five arms against one oracle dump.
|
|
* --pair ARM ORACLE
|
|
* read both dumps, score every position, apply the criteria, print the
|
|
* verdict. Loads NO model and touches NO card, so it costs seconds.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* SCORING A PREFILL ARM: `--prefill L`
|
|
* ---------------------------------------------------------------------------
|
|
* The batched prefill produces NO per-position logits — the last prompt token goes through the
|
|
* ordinary single-token forward and that is the only row a prefill emits. So a prefill arm
|
|
* cannot be scored the way a decode arm is, position against position.
|
|
*
|
|
* What CAN be scored, and is the question a serve actually asks, is the STATE a prefill
|
|
* leaves. `--prefill L` runs the first `L` positions of every document through
|
|
* `pn_qwen35moe_prefill_tt()` — the shipping entry point, the same one `pn_prefill_tt_batched`
|
|
* calls — and then decodes and scores positions `L-1 .. n-1` one at a time. Those decode steps
|
|
* are byte-for-byte the same code in every arm; the ONLY thing that differs between arms is
|
|
* the conv window, the recurrent state and the K/V the prefill left behind. Any divergence the
|
|
* chunkwise mixers introduce is therefore carried into every scored position, autoregressively.
|
|
*
|
|
* The SEQUENTIAL prefill arm needs no separate mode: with `PN_TT_PREFILL` unset the shipping
|
|
* entry point returns NULL WITHOUT TOUCHING THE STATE, and the same `L` positions run through
|
|
* the per-token loop instead. Same scored set, same code, one flag. `--require-prefill` turns
|
|
* a refusal into an abort, because "the chunkwise arm silently ran the sequential path" is the
|
|
* one way this measurement can lie and read plausible.
|
|
*
|
|
* L IS CHOSEN SO THE PREFILL SPANS MORE THAN ONE CHUNK. Every red arm this family has —
|
|
* `gdnc_window_wipe`, `gdnc_state_wipe`, attention's `prior_wipe` — is INVISIBLE IN CHUNK 0,
|
|
* because the incoming window and state are zero there and half the transform is not
|
|
* exercised. A single-chunk prefill would be a gate those bugs pass. At the default 32 rows,
|
|
* `L = 97` is three full chunks (0-31, 32-63, 64-95) plus the forward that emits position 96.
|
|
*
|
|
* A document shorter than `L + 2` ids is SKIPPED, identically in both passes, and said so out
|
|
* loud — the alternative is a document whose "prefill" is the whole of it.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT KEEPS TWO PASSES HONEST
|
|
* ---------------------------------------------------------------------------
|
|
* Two passes equal one interleaved pass only if they scored the same positions of the same
|
|
* token stream. That is CHECKED, not assumed:
|
|
*
|
|
* - each record carries its own `(document, position, next_token)` and pairing refuses on
|
|
* the first mismatch, naming the record;
|
|
* - the headers must agree on vocabulary, architecture, framing and record count, so a
|
|
* `--plain` on one pass and not the other is a refusal rather than a misalignment;
|
|
* - a dump whose writer died before closing records ZERO positions and is refused, instead
|
|
* of pairing a truncated arm against a whole oracle;
|
|
* - `argmax` is written by the producer and re-derived by the pairer, so a row read back at
|
|
* the wrong offset is caught rather than scored.
|
|
*
|
|
* The two arms' `pn_state`s were already independent inside the interleaved gate, so nothing
|
|
* but host load and card temperature was ever held common — and neither is an input to the
|
|
* arithmetic.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* PROVING IT CAN FAIL — AND THIS ONE NEEDS NO CARD AND NO MODEL
|
|
* ---------------------------------------------------------------------------
|
|
* `--pair D D --noise SIGMA` pairs a dump against ITSELF. At sigma 0 every figure must be
|
|
* exactly zero-divergence and the gate additionally FAILS if it is not; at the sigmas the
|
|
* thresholds were calibrated against it must FAIL (0.2 costs ratio 1.0091 and 9.09% disagree
|
|
* against an 8% limit; 0.4 costs 1.0902). A gate that still says PASS at sigma 0.4 is
|
|
* measuring nothing. The whole red ladder is three invocations of the pairing step over one
|
|
* cached dump — seconds, against the ~3 h an interleaved red arm costs.
|
|
*
|
|
* USAGE
|
|
* ./tests/test_two_pass_parity_gate --arm ARM.bin <model.gguf> [--corpus F] [--plain]
|
|
* [--max-pos N] [--prefill L] [--require-prefill]
|
|
* ./tests/test_two_pass_parity_gate --oracle ORACLE.bin <model.gguf> [--corpus F] [--plain]
|
|
* [--max-pos N] [--prefill L]
|
|
* ./tests/test_two_pass_parity_gate --pair ARM.bin ORACLE.bin [--noise SIGMA]
|
|
*
|
|
* NOT part of the default `make test`: the dump modes need a model file, and the arm mode
|
|
* needs a card.
|
|
*
|
|
* @status Working
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
#include "../include/pantheon.h"
|
|
#include "../src/core/engine.h"
|
|
#include "../src/models/qwen35moe.h"
|
|
#include "tensor/tensor.h"
|
|
|
|
#include "chat_corpus.h"
|
|
#include "parity_accel_arm.h"
|
|
#include "parity_gate_criteria.h"
|
|
#include "parity_logit_dump.h"
|
|
#include "parity_noise.h"
|
|
#include "parity_score.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#define GATE_DEFAULT_CORPUS "tests/data/chat_parity_corpus.txt"
|
|
|
|
/** How much of each document to echo back, decoded, so a reader can SEE it is real text. */
|
|
#define GATE_ECHO_CHARS 72
|
|
|
|
/** The red arm's perturbation stream. One seeded stream for the whole pairing, so a repeat of
|
|
* the same command line draws the same noise and the arm is reproducible. */
|
|
#define GATE_NOISE_SEED 0x9E3779B97F4A7C15ULL
|
|
|
|
typedef enum { GATE_MODE_NONE, GATE_MODE_ARM, GATE_MODE_ORACLE, GATE_MODE_PAIR } gate_mode;
|
|
|
|
/** Where one document's wall time went: the forward (on CUDA this includes the logit-row
|
|
* download, which is synchronous inside it) against the row check and the dump write. */
|
|
typedef struct {
|
|
int forwards;
|
|
double forward_s;
|
|
double row_s;
|
|
} gate_document_timing;
|
|
|
|
static double gate_monotonic_seconds(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The dump passes
|
|
* ================================================================ */
|
|
|
|
/**
|
|
* Tokenise one document in the requested framing and echo what it decodes back to.
|
|
*
|
|
* The echo is not decoration: the gate this family replaces was fed ids from another model's
|
|
* tokeniser and nobody looked.
|
|
*
|
|
* @return number of ids, or -1
|
|
*/
|
|
static int gate_tokenise_document(pn_model *model, const chat_document *doc, int chat_arm,
|
|
uint32_t *ids, int index)
|
|
{
|
|
int n_ids = chat_arm ? chat_document_to_chat_ids(model, doc, ids, CHAT_CORPUS_MAX_IDS)
|
|
: chat_document_to_plain_ids(model, doc, ids, CHAT_CORPUS_MAX_IDS);
|
|
char *text;
|
|
|
|
if (n_ids <= 0)
|
|
return -1;
|
|
text = chat_ids_to_text(model, ids, n_ids);
|
|
printf("DUMP doc=%d ids=%d framing=%s text=\"", index, n_ids, chat_arm ? "chat" : "plain");
|
|
for (int i = 0; text && text[i] && i < GATE_ECHO_CHARS; i++)
|
|
putchar(text[i] == '\n' ? ' ' : text[i]);
|
|
printf("...\"\n");
|
|
free(text);
|
|
fflush(stdout);
|
|
return n_ids;
|
|
}
|
|
|
|
/**
|
|
* Refuse a SPARSE logit row before a single position of it is scored.
|
|
*
|
|
* **THIS IS A REAL TRAP ON BOTH ACCELERATORS, NOT A HYPOTHETICAL.** On the card,
|
|
* `PN_TT_SPARSE_LOGITS=1` is on in every speed arm this project runs: it selects the exact
|
|
* top-8 and the host fills the other 248,312 entries with `-INFINITY`. A greedy argmax over
|
|
* that row is exactly the argmax over the dense one — which is why it ships — but a
|
|
* perplexity, a KL and a paired t over it are all meaningless, and `disagree_rate` would read
|
|
* 0% while every other figure was noise. A gate that scored it would be the eighth
|
|
* vacuous-gate class in this project.
|
|
*
|
|
* **THE CUDA ARM IS SPARSE BY DEFAULT, WHICH IS WORSE — MEASURED ON A K80 2026-08-24.**
|
|
* `gpu_topk_k()` returns 40 unless told otherwise, so `pn_qwen35moe_forward_cuda` reduces the
|
|
* head ON the die and presents 40 survivors against 248,280 `-INFINITY`. Nothing has to be
|
|
* switched on for this: an Ornith CUDA parity pass that nobody configured is sparse. The knob
|
|
* is `QWEN35_GPU_FULL_LOGITS=1`, and it changes only the PRESENTATION — the head matmul and
|
|
* the top-k reduction are documented bit-exact, so the full-logits arm is the same arm, fully
|
|
* observable. This guard is what caught it, on the first row, before 339 positions of noise.
|
|
*
|
|
* Checked ONCE, on the first row of the run, because the presentation is a property of the
|
|
* configuration and not of the position.
|
|
*
|
|
* @return 0 when the row is dense, -1 with the reason on stderr
|
|
*/
|
|
static int gate_refuse_sparse_row(const float *row, int n)
|
|
{
|
|
int non_finite = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
if (!(row[i] > -1e30f && row[i] < 1e30f))
|
|
non_finite++;
|
|
}
|
|
if (non_finite * 2 < n)
|
|
return 0;
|
|
fprintf(stderr, "dump: REFUSED — %d of %d logits in the first row are not finite. This is "
|
|
"a SPARSE logit row, which carries the exact top-N and -INFINITY "
|
|
"everywhere else. The argmax over it is right and every other figure this "
|
|
"gate computes would be noise. Fix it at the arm that produced it: on the "
|
|
"card UNSET PN_TT_SPARSE_LOGITS (opt-in); on CUDA SET "
|
|
"QWEN35_GPU_FULL_LOGITS=1, because there the device top-k is the DEFAULT "
|
|
"(k=40) and a pass nobody configured is already sparse.\n", non_finite, n);
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Put a NaN into one logit of the `n`th checked row, so the guard below can be proven to fire.
|
|
*
|
|
* `PN_PARITY_INJECT_NONFINITE=N` — N counted over checked rows, 0 for the first. It needs a
|
|
* card arm of three positions, not a corpus and not an oracle, so the red ladder for this
|
|
* guard costs one model load.
|
|
*
|
|
* The NaN is built from BITS. A `0.0f / 0.0f` would be folded to a constant — or to nothing —
|
|
* under this tree's `-ffast-math`, and a red arm that cannot produce the condition it is
|
|
* testing for is the eleventh vacuous-gate class rather than a proof.
|
|
*/
|
|
static void gate_inject_non_finite(float *row, int n, int checked_index)
|
|
{
|
|
const char *env = getenv("PN_PARITY_INJECT_NONFINITE");
|
|
uint32_t quiet_nan = 0x7FC00000u;
|
|
|
|
if (!env || !*env || atoi(env) != checked_index || n < 1)
|
|
return;
|
|
memcpy(&row[0], &quiet_nan, sizeof(quiet_nan));
|
|
fprintf(stderr, "PN_PARITY_INJECT_NONFINITE: logit 0 of checked row %d is now a NaN. The "
|
|
"dump must REFUSE.\n", checked_index);
|
|
}
|
|
|
|
/**
|
|
* Refuse a row carrying ANY non-finite logit, before it is written.
|
|
*
|
|
* **DO NOT INFER THE ABSENCE OF A NaN FROM A SMALL MAXIMUM.** Every comparison against a NaN
|
|
* is false, so a `max` never selects one and a column full of them reads as zero divergence —
|
|
* a broken chunkwise arm scored 7.775e-15 that way and read green. This counts them instead,
|
|
* on EVERY row rather than the first, because a NaN in this family is data-dependent (the
|
|
* `P_last / P_t` underflow appeared six positions into a chunk and only at real decays).
|
|
*
|
|
* @return 0 when the row is entirely finite, -1 with the count and the position on stderr
|
|
*/
|
|
static int gate_refuse_non_finite_row(const float *row, int n, int document, int position)
|
|
{
|
|
int non_finite = 0;
|
|
|
|
for (int i = 0; i < n; i++)
|
|
if (!parity_float_is_finite(row[i]))
|
|
non_finite++;
|
|
if (non_finite == 0)
|
|
return 0;
|
|
fprintf(stderr, "dump: REFUSED — document %d position %d has %d of %d logits non-finite. "
|
|
"A max over such a row silently drops them and scores near zero; this "
|
|
"gate counts them instead.\n", document, position, non_finite, n);
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Check one returned row and write it if it is scored.
|
|
*
|
|
* @param scored 0 to run the position without recording it (a prefill prologue, or the last
|
|
* position of a document, which has no label)
|
|
* @return 0 on success, -1 on a refusal or a short write
|
|
*/
|
|
static int gate_take_row(tm_tensor *logits, parity_dump_writer *writer, int document,
|
|
int position, int next_token, int scored)
|
|
{
|
|
static int presentation_checked;
|
|
static int checked_rows;
|
|
|
|
/* THE ROW THE FORWARD RETURNED HAS TO BE THE WIDTH THE HEADER PROMISED. The writer scans
|
|
* and stores `n_vocab` floats taken from the header; a forward returning a shorter row
|
|
* would make every record an overread of the one after it, and the pairing step's own
|
|
* argmax check would then be comparing two consistent readings of the wrong bytes.
|
|
* Checked once, because a width is a property of the model, as is the sparse presentation. */
|
|
if (!presentation_checked) {
|
|
presentation_checked = 1;
|
|
if ((uint32_t)logits->shape[0] != writer->n_vocab) {
|
|
fprintf(stderr, "dump: REFUSED — the forward returned a %d-wide row and the model "
|
|
"declares %u\n", (int)logits->shape[0], writer->n_vocab);
|
|
return -1;
|
|
}
|
|
if (gate_refuse_sparse_row(tm_data_f32(logits), (int)logits->shape[0]) != 0)
|
|
return -1;
|
|
}
|
|
gate_inject_non_finite(tm_data_f32(logits), (int)logits->shape[0], checked_rows++);
|
|
if (gate_refuse_non_finite_row(tm_data_f32(logits), (int)logits->shape[0], document,
|
|
position) != 0)
|
|
return -1;
|
|
if (!scored)
|
|
return 0;
|
|
return parity_dump_write_row(writer, document, position, next_token, tm_data_f32(logits));
|
|
}
|
|
|
|
/**
|
|
* The PRODUCT's batched prefill, or a refusal that leaves the state untouched.
|
|
*
|
|
* This is the shipping entry point `pn_prefill_tt_batched()` calls, reached one level down
|
|
* because that wrapper is static to the engine. Calling anything else would score a forward
|
|
* the serve does not run.
|
|
*
|
|
* @return the logits for position `n - 1` (caller owns), or NULL for NOT TAKEN
|
|
*/
|
|
static tm_tensor *gate_batched_prefill(pn_model *model, const uint32_t *toks, int n,
|
|
pn_state *state)
|
|
{
|
|
#ifdef USE_TT
|
|
if (model->forward == pn_qwen35moe_forward_tt)
|
|
return pn_qwen35moe_prefill_tt(model, toks, n, state);
|
|
#endif
|
|
(void)model;
|
|
(void)toks;
|
|
(void)n;
|
|
(void)state;
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Step ONE arm through one token sequence, writing every scored position's row.
|
|
*
|
|
* The forward and the seam are set ONCE by the caller and never touched here — that is the
|
|
* whole point of a single-arm pass, and switching either mid-sequence is what the interleaved
|
|
* gate does.
|
|
*
|
|
* @param prefill run positions `0 .. prefill-1` through the batched prefill and score
|
|
* from `prefill-1` onward; 0 disables and every position is scored
|
|
* @param prefill_taken set to 1 if the batched entry point ran, 0 if it declined
|
|
* @param timing accumulates this document's forward count and wall split
|
|
* @return 0 on success, -1 if a forward or a write failed
|
|
*/
|
|
static int gate_dump_sequence(pn_model *model, pn_forward_fn forward, const uint32_t *toks,
|
|
int ntok, int document, parity_dump_writer *writer, int prefill,
|
|
int *prefill_taken, gate_document_timing *timing)
|
|
{
|
|
pn_state *state = pn_state_new(model);
|
|
int first = 0, rc = 0;
|
|
|
|
*prefill_taken = 0;
|
|
if (!state) {
|
|
fprintf(stderr, "dump: state alloc failed\n");
|
|
return -1;
|
|
}
|
|
if (prefill >= 2 && prefill < ntok) {
|
|
tm_tensor *logits = gate_batched_prefill(model, toks, prefill, state);
|
|
|
|
/* A REFUSAL IS NOT A FAILURE — it is the sequential arm. The entry point returns NULL
|
|
* without having touched the state, so falling through to the per-token loop from
|
|
* position 0 scores exactly the same positions by exactly the same code. */
|
|
if (logits) {
|
|
*prefill_taken = 1;
|
|
rc = gate_take_row(logits, writer, document, prefill - 1, (int)toks[prefill], 1);
|
|
tm_free(logits);
|
|
first = prefill;
|
|
}
|
|
}
|
|
for (int i = first; rc == 0 && i < ntok; i++) {
|
|
double t0 = gate_monotonic_seconds();
|
|
tm_tensor *logits = forward(model, toks[i], state);
|
|
double t1 = gate_monotonic_seconds();
|
|
|
|
timing->forwards++;
|
|
timing->forward_s += t1 - t0;
|
|
|
|
if (!logits) {
|
|
fprintf(stderr, "dump: forward failed at document %d position %d\n", document, i);
|
|
rc = -1;
|
|
break;
|
|
}
|
|
/* The last position has no label in the corpus, so it is RUN — the state must advance
|
|
* exactly as the interleaved gate advanced it — and not scored. Neither is the prefill
|
|
* prologue, which exists to build the state the scored positions then read. */
|
|
rc = gate_take_row(logits, writer, document, i, i + 1 < ntok ? (int)toks[i + 1] : 0,
|
|
i + 1 < ntok && i >= prefill - 1);
|
|
tm_free(logits);
|
|
timing->row_s += gate_monotonic_seconds() - t1;
|
|
}
|
|
pn_state_free(state);
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* Run one whole pass — arm or oracle — over the corpus and write the dump.
|
|
*
|
|
* @return 0 on success, 1 on any failure (the process exit code)
|
|
*/
|
|
static int gate_run_dump_pass(const char *model_path, const char *corpus_path,
|
|
const char *dump_path, int chat_arm, int max_pos, gate_mode mode,
|
|
int allow_armed_oracle, int prefill, int require_prefill)
|
|
{
|
|
static uint32_t ids[CHAT_CORPUS_MAX_IDS];
|
|
parity_dump_writer writer;
|
|
const char *arm_name = "CPU", *cpu_name = "CPU";
|
|
chat_document doc = {0};
|
|
pn_forward_fn forward, cpu;
|
|
pn_model *model;
|
|
FILE *corpus;
|
|
int docs = 0, seam, taken_docs = 0, skipped_docs = 0, rc = 0;
|
|
double load_t0;
|
|
|
|
corpus = fopen(corpus_path, "r");
|
|
if (!corpus) {
|
|
fprintf(stderr, "dump: cannot open corpus %s\n", corpus_path);
|
|
return 1;
|
|
}
|
|
load_t0 = gate_monotonic_seconds();
|
|
model = pn_load(model_path);
|
|
if (!model) {
|
|
fprintf(stderr, "dump: load failed\n");
|
|
fclose(corpus);
|
|
return 1;
|
|
}
|
|
cpu = parity_select_cpu_oracle(model, &cpu_name);
|
|
if (!cpu) {
|
|
pn_model_free(model);
|
|
fclose(corpus);
|
|
return 1;
|
|
}
|
|
if (mode == GATE_MODE_ARM) {
|
|
forward = parity_select_accel_arm(model, &arm_name);
|
|
if (!forward) {
|
|
pn_model_free(model);
|
|
fclose(corpus);
|
|
return 1;
|
|
}
|
|
/* The identity control makes the arm the oracle, so the seam must stay DETACHED for
|
|
* it too — leaving it attached would run the control on the card. */
|
|
seam = !parity_identity_control_active();
|
|
} else {
|
|
forward = cpu;
|
|
arm_name = cpu_name;
|
|
seam = 0;
|
|
}
|
|
/* The return says whether a seam WAS attached, which is the only way to find out that an
|
|
* oracle pass is running inside a card-armed process. That is not merely wasteful: it
|
|
* leaves every resident triad armed and idle for the ~3 h the oracle takes, which is the
|
|
* exact condition this harness exists to remove. Run the oracle pass from a build or a
|
|
* configuration that never armed the card. */
|
|
if (parity_accel_seam_enable(model, seam) && mode == GATE_MODE_ORACLE) {
|
|
fprintf(stderr, "dump: REFUSED — this is the ORACLE pass and the accelerator seam was "
|
|
"ATTACHED, so the card is armed. Every resident triad would sit armed "
|
|
"and idle for the whole pass, which is the defect the two-pass design "
|
|
"exists to remove. Run the oracle pass unarmed, or pass "
|
|
"--allow-armed-oracle if you have a reason.\n");
|
|
if (!allow_armed_oracle) {
|
|
pn_model_free(model);
|
|
fclose(corpus);
|
|
return 1;
|
|
}
|
|
}
|
|
model->forward = forward;
|
|
|
|
printf("DUMP mode=%s arm=%s oracle=%s arch=%d seam=%s corpus=%s framing=%s n_vocab=%d "
|
|
"prefill=%d out=%s load_s=%.1f\n", mode == GATE_MODE_ARM ? "arm" : "oracle", arm_name,
|
|
cpu_name, (int)model->config.arch, seam ? "attached" : "detached", corpus_path,
|
|
chat_arm ? "chat" : "plain", model->config.n_vocab, prefill, dump_path,
|
|
gate_monotonic_seconds() - load_t0);
|
|
fflush(stdout);
|
|
|
|
if (parity_dump_open_write(dump_path, (uint32_t)model->config.n_vocab,
|
|
(uint32_t)model->config.arch, chat_arm, seam, arm_name,
|
|
&writer) != 0) {
|
|
pn_model_free(model);
|
|
fclose(corpus);
|
|
return 1;
|
|
}
|
|
|
|
while (chat_document_read(corpus, &doc) && docs < PARITY_GATE_MAX_DOCUMENTS) {
|
|
int n_ids = gate_tokenise_document(model, &doc, chat_arm, ids, docs);
|
|
gate_document_timing timing = {0};
|
|
int taken = 0;
|
|
|
|
chat_document_free(&doc);
|
|
if (n_ids <= 0)
|
|
continue;
|
|
if (max_pos > 0 && n_ids > max_pos)
|
|
n_ids = max_pos;
|
|
/* SKIPPED IDENTICALLY IN BOTH PASSES, because the rule reads only `n_ids` and the two
|
|
* passes tokenise the same corpus with the same model. A document whose "prefill" is
|
|
* the whole of it scores nothing and would silently shorten one side of the pairing. */
|
|
if (prefill >= 2 && n_ids < prefill + 2) {
|
|
printf("DUMP doc=%d SKIPPED — %d ids is short of prefill %d + 2\n", docs, n_ids,
|
|
prefill);
|
|
fflush(stdout);
|
|
skipped_docs++;
|
|
continue;
|
|
}
|
|
if (gate_dump_sequence(model, forward, ids, n_ids, docs, &writer, prefill,
|
|
&taken, &timing) != 0) {
|
|
fprintf(stderr, "dump: document %d failed — refusing to close a partial dump\n",
|
|
docs);
|
|
rc = 1;
|
|
break;
|
|
}
|
|
taken_docs += taken;
|
|
docs++;
|
|
printf("DUMP doc=%d complete, %llu records so far, prefill=%s scored_from=%d "
|
|
"forwards=%d forward_s=%.1f (%.4f s each) row_s=%.1f\n",
|
|
docs - 1, (unsigned long long)writer.records,
|
|
prefill >= 2 ? (taken ? "TAKEN(batched)" : "declined(per-token)") : "off",
|
|
prefill >= 2 ? prefill - 1 : 0, timing.forwards, timing.forward_s,
|
|
timing.forwards ? timing.forward_s / timing.forwards : 0.0, timing.row_s);
|
|
fflush(stdout);
|
|
}
|
|
fclose(corpus);
|
|
|
|
/* THE ONE WAY THIS MEASUREMENT CAN LIE AND STILL READ PLAUSIBLE. A chunkwise arm whose
|
|
* batched entry point quietly declined is the SEQUENTIAL arm wearing its label, and every
|
|
* figure downstream would be a real number about the wrong thing. */
|
|
if (rc == 0 && require_prefill && taken_docs != docs) {
|
|
fprintf(stderr, "dump: REFUSED — --require-prefill, and the batched prefill was taken "
|
|
"on %d of %d documents. An arm that declined is the SEQUENTIAL path, "
|
|
"not the one this dump is labelled with.\n", taken_docs, docs);
|
|
rc = 1;
|
|
}
|
|
|
|
/* A failed pass deliberately does NOT patch the record count, so the file reads as empty
|
|
* and the pairing step refuses it rather than scoring a truncated arm. */
|
|
if (rc == 0 && parity_dump_close_write(&writer) != 0)
|
|
rc = 1;
|
|
if (rc == 0)
|
|
printf("DUMP COMPLETE documents=%d skipped=%d prefill_taken=%d records=%llu file=%s\n",
|
|
docs, skipped_docs, taken_docs, (unsigned long long)writer.records, dump_path);
|
|
|
|
/* NOT _Exit: the TT backend's close retires the on-card residencies by ASKING, and
|
|
* skipping it leaves four cores spinning on an armed queue the next probe refuses. */
|
|
pn_model_free(model);
|
|
return rc;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The pairing pass
|
|
* ================================================================ */
|
|
|
|
/** The perturbation applied to the ARM's rows at pairing time, if any. */
|
|
static double g_noise_sigma;
|
|
static parity_noise_rng g_noise_rng;
|
|
|
|
/**
|
|
* Score one arm dump against one oracle dump and print the verdict.
|
|
*
|
|
* @param identity the two paths named the same file — every figure must be exactly zero
|
|
* @return 0 pass, 1 fail or refusal
|
|
*/
|
|
static int gate_run_pair_pass(const char *arm_path, const char *oracle_path, int identity)
|
|
{
|
|
static parity_score per_doc[PARITY_GATE_MAX_DOCUMENTS];
|
|
parity_dump_reader arm = {0}, oracle = {0};
|
|
parity_score total = {0};
|
|
parity_summary summary;
|
|
float *arm_row = NULL, *oracle_row = NULL, *noise_row = NULL;
|
|
uint32_t n_vocab;
|
|
int docs = 0, rc = 1;
|
|
uint64_t index;
|
|
|
|
if (parity_dump_open_read(arm_path, &arm) != 0)
|
|
return 1;
|
|
if (parity_dump_open_read(oracle_path, &oracle) != 0) {
|
|
parity_dump_close_read(&arm);
|
|
return 1;
|
|
}
|
|
if (parity_dump_headers_agree(&arm.header, &oracle.header) != 0)
|
|
goto cleanup;
|
|
|
|
n_vocab = arm.header.n_vocab;
|
|
arm_row = malloc((size_t)n_vocab * sizeof(*arm_row));
|
|
oracle_row = malloc((size_t)n_vocab * sizeof(*oracle_row));
|
|
noise_row = g_noise_sigma > 0.0 ? malloc((size_t)n_vocab * sizeof(*noise_row)) : NULL;
|
|
if (!arm_row || !oracle_row || (g_noise_sigma > 0.0 && !noise_row)) {
|
|
fprintf(stderr, "pair: two %u-wide rows did not allocate\n", n_vocab);
|
|
goto cleanup;
|
|
}
|
|
|
|
printf("PAIR arm=%s(%s) oracle=%s(%s) arch=%u framing=%s records=%llu noise=%.4g%s\n",
|
|
arm_path, arm.header.label, oracle_path, oracle.header.label, arm.header.arch,
|
|
arm.header.framing_is_chat ? "chat" : "plain",
|
|
(unsigned long long)arm.header.record_count, g_noise_sigma,
|
|
identity ? " IDENTITY-CONTROL(same file both sides)" : "");
|
|
if (arm.header.seam_enabled == 0u && !identity)
|
|
printf("PAIR WARNING arm dump records seam=detached — that arm was NOT the "
|
|
"accelerator\n");
|
|
fflush(stdout);
|
|
|
|
for (index = 0; index < arm.header.record_count; index++) {
|
|
parity_dump_record ra, ro;
|
|
const float *scored;
|
|
int a1 = 0, a2 = 0, c1 = 0, c2 = 0;
|
|
|
|
if (parity_dump_read_row(&arm, &ra, arm_row) != 1 ||
|
|
parity_dump_read_row(&oracle, &ro, oracle_row) != 1) {
|
|
fprintf(stderr, "pair: a dump ended early at record %llu\n",
|
|
(unsigned long long)index);
|
|
goto cleanup;
|
|
}
|
|
/* THE ALIGNMENT CHECK IS THE WHOLE ARGUMENT FOR PAIRING OFFLINE. Two passes are one
|
|
* interleaved pass only if they scored the same positions of the same stream. */
|
|
if (ra.document != ro.document || ra.position != ro.position ||
|
|
ra.next_token != ro.next_token) {
|
|
fprintf(stderr, "pair: REFUSED — record %llu is arm(doc %d pos %d next %d) against "
|
|
"oracle(doc %d pos %d next %d). The two passes did not score the "
|
|
"same stream.\n", (unsigned long long)index, ra.document,
|
|
ra.position, ra.next_token, ro.document, ro.position, ro.next_token);
|
|
goto cleanup;
|
|
}
|
|
if (ra.document < 0 || ra.document >= PARITY_GATE_MAX_DOCUMENTS) {
|
|
fprintf(stderr, "pair: record %llu names document %d\n",
|
|
(unsigned long long)index, ra.document);
|
|
goto cleanup;
|
|
}
|
|
if (ra.document + 1 > docs)
|
|
docs = ra.document + 1;
|
|
|
|
scored = arm_row;
|
|
if (noise_row) {
|
|
parity_noise_add(noise_row, arm_row, (int)n_vocab, g_noise_sigma, &g_noise_rng);
|
|
scored = noise_row;
|
|
}
|
|
parity_top2_indices(scored, (int)n_vocab, &a1, &a2);
|
|
parity_top2_indices(oracle_row, (int)n_vocab, &c1, &c2);
|
|
/* THE PRODUCER'S OWN ARGMAX, RE-DERIVED. A row read back at the wrong offset scores a
|
|
* real-looking number; this catches it. It is skipped under --noise, where the
|
|
* perturbed argmax is expected to move. */
|
|
if (!noise_row && a1 != ra.argmax) {
|
|
fprintf(stderr, "pair: REFUSED — record %llu re-derives argmax %d and the arm dump "
|
|
"recorded %d. The row was read at the wrong offset.\n",
|
|
(unsigned long long)index, a1, ra.argmax);
|
|
goto cleanup;
|
|
}
|
|
if (c1 != ro.argmax) {
|
|
fprintf(stderr, "pair: REFUSED — record %llu re-derives the oracle's argmax as %d "
|
|
"and the dump recorded %d.\n", (unsigned long long)index, c1,
|
|
ro.argmax);
|
|
goto cleanup;
|
|
}
|
|
parity_score_add(&per_doc[ra.document], scored, oracle_row, (int)n_vocab,
|
|
ra.next_token, a1, c1, NULL);
|
|
}
|
|
|
|
for (int i = 0; i < docs; i++) {
|
|
char label[64];
|
|
|
|
snprintf(label, sizeof(label), "doc=%d", i);
|
|
parity_score_report(&per_doc[i], label);
|
|
parity_score_merge(&total, &per_doc[i]);
|
|
}
|
|
parity_score_summarise(&total, &summary);
|
|
printf("GATE documents=%d scored_positions=%ld\n", docs, summary.n);
|
|
parity_score_report(&total, "GATE TOTAL");
|
|
rc = parity_gate_verdict(&summary, per_doc, docs, identity, g_noise_sigma,
|
|
arm.header.framing_is_chat ? "chat" : "plain");
|
|
|
|
cleanup:
|
|
free(arm_row);
|
|
free(oracle_row);
|
|
free(noise_row);
|
|
parity_dump_close_read(&arm);
|
|
parity_dump_close_read(&oracle);
|
|
return rc;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Command line
|
|
* ================================================================ */
|
|
|
|
static void gate_usage(const char *argv0)
|
|
{
|
|
fprintf(stderr,
|
|
"usage:\n"
|
|
" %s --arm ARM.bin <model.gguf> [--corpus F] [--plain] [--max-pos N]\n"
|
|
" [--prefill L] [--require-prefill]\n"
|
|
" %s --oracle ORACLE.bin <model.gguf> [--corpus F] [--plain] [--max-pos N]\n"
|
|
" [--prefill L] [--allow-armed-oracle]\n"
|
|
" %s --pair ARM.bin ORACLE.bin [--noise SIGMA]\n"
|
|
"\n"
|
|
"The red ladder needs no card and no model:\n"
|
|
" %s --pair ORACLE.bin ORACLE.bin must be EXACTLY zero divergence\n"
|
|
" %s --pair ORACLE.bin ORACLE.bin --noise 0.2 must FAIL (disagree 9.09%%)\n"
|
|
" %s --pair ORACLE.bin ORACLE.bin --noise 0.4 must FAIL (ratio 1.0902)\n",
|
|
argv0, argv0, argv0, argv0, argv0, argv0);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
const char *corpus_path = GATE_DEFAULT_CORPUS;
|
|
const char *dump_path = NULL, *oracle_path = NULL, *model_path = NULL;
|
|
gate_mode mode = GATE_MODE_NONE;
|
|
int chat_arm = 1, max_pos = 0, allow_armed_oracle = 0, prefill = 0, require_prefill = 0;
|
|
|
|
parity_noise_seed(&g_noise_rng, GATE_NOISE_SEED);
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--arm") == 0 && i + 1 < argc) {
|
|
mode = GATE_MODE_ARM;
|
|
dump_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--oracle") == 0 && i + 1 < argc) {
|
|
mode = GATE_MODE_ORACLE;
|
|
dump_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--pair") == 0 && i + 2 < argc) {
|
|
mode = GATE_MODE_PAIR;
|
|
dump_path = argv[++i];
|
|
oracle_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--corpus") == 0 && i + 1 < argc) {
|
|
corpus_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--max-pos") == 0 && i + 1 < argc) {
|
|
max_pos = atoi(argv[++i]);
|
|
} else if (strcmp(argv[i], "--prefill") == 0 && i + 1 < argc) {
|
|
prefill = atoi(argv[++i]);
|
|
} else if (strcmp(argv[i], "--require-prefill") == 0) {
|
|
require_prefill = 1;
|
|
} else if (strcmp(argv[i], "--noise") == 0 && i + 1 < argc) {
|
|
g_noise_sigma = atof(argv[++i]);
|
|
} else if (strcmp(argv[i], "--plain") == 0) {
|
|
chat_arm = 0;
|
|
} else if (strcmp(argv[i], "--allow-armed-oracle") == 0) {
|
|
allow_armed_oracle = 1;
|
|
} else if (!model_path && argv[i][0] != '-') {
|
|
model_path = argv[i];
|
|
} else {
|
|
fprintf(stderr, "gate: unknown argument %s\n", argv[i]);
|
|
gate_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (mode == GATE_MODE_PAIR) {
|
|
if (g_noise_sigma < 0.0) {
|
|
fprintf(stderr, "gate: --noise must not be negative\n");
|
|
return 1;
|
|
}
|
|
/* Same file on both sides IS the identity control, and it needs no environment
|
|
* variable to say so — which is one fewer way for a card run to be mistaken for a
|
|
* control. */
|
|
return gate_run_pair_pass(dump_path, oracle_path,
|
|
strcmp(dump_path, oracle_path) == 0);
|
|
}
|
|
if (mode == GATE_MODE_NONE || !model_path) {
|
|
gate_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
if (prefill == 1) {
|
|
fprintf(stderr, "gate: --prefill 1 is not a batched pass and not a control either; use "
|
|
"0 to disable or a length that spans more than one chunk\n");
|
|
return 1;
|
|
}
|
|
if (require_prefill && mode != GATE_MODE_ARM) {
|
|
fprintf(stderr, "gate: --require-prefill is an ARM check; the oracle has no batched "
|
|
"path to require\n");
|
|
return 1;
|
|
}
|
|
return gate_run_dump_pass(model_path, corpus_path, dump_path, chat_arm, max_pos, mode,
|
|
allow_armed_oracle, prefill, require_prefill);
|
|
}
|