From 0d1806c99f8c4cfd0b32527441aa2d344a3627fd Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:43:48 +0000 Subject: [PATCH] fix(prism): ban non-causal TokenMix label-leak class Catch MLP-Mixer/TokenMix sequence mixers without a causal mask before pod rent (static screen + agentic code), and document the causal LM ban for miners. --- crates/challenge-agentic/src/lib.rs | 4 +- crates/challenge-agentic/src/prompts.rs | 9 +- crates/challenge-agentic/src/sim.rs | 132 ++++++++++++++--- crates/challenge-agentic/src/tools.rs | 2 + crates/challenge-agentic/src/types.rs | 3 + crates/challenge-ast/src/lib.rs | 3 +- crates/challenge-ast/src/source_cheats.rs | 157 +++++++++++++++++++++ crates/prism-challenge/src/orchestrator.rs | 5 +- docs/PRISM.md | 7 +- docs/PRISM_RECIPE.md | 9 ++ docs/external-miner/prism.md | 11 ++ 11 files changed, 313 insertions(+), 29 deletions(-) diff --git a/crates/challenge-agentic/src/lib.rs b/crates/challenge-agentic/src/lib.rs index 0848f07ff..86065efe5 100644 --- a/crates/challenge-agentic/src/lib.rs +++ b/crates/challenge-agentic/src/lib.rs @@ -23,8 +23,8 @@ mod types; pub use agent::{AgentConfig, OpenRouterAgent}; pub use challenge_ast::{ - copy_gate, static_source_cheat, training_has_telemetry_hooks, CopyGateHit, GateCorpusEntry, - SourceCheatHit, SourceCheatKind, + arch_has_noncausal_seq_mix, copy_gate, static_source_cheat, training_has_telemetry_hooks, + CopyGateHit, GateCorpusEntry, SourceCheatHit, SourceCheatKind, }; pub use llm::{load_api_key_file, DEFAULT_MODEL}; pub use prompts::{AGENTIC_PROMPT_VERSION, DESIGN_DOMAIN_RULES, PRISM_DOMAIN_RULES}; diff --git a/crates/challenge-agentic/src/prompts.rs b/crates/challenge-agentic/src/prompts.rs index c2603ed7c..788c08529 100644 --- a/crates/challenge-agentic/src/prompts.rs +++ b/crates/challenge-agentic/src/prompts.rs @@ -1,17 +1,18 @@ //! System prompt for the agentic anti-cheat loop. /// Prompt version stamp (audit). -pub const AGENTIC_PROMPT_VERSION: &str = "agentic_v2"; +pub const AGENTIC_PROMPT_VERSION: &str = "agentic_v3"; /// Prism-specific anti-cheat rules (metrics / eval short-circuit / AST copy / -/// telemetry-hook contract). +/// telemetry-hook contract / non-causal label leak). pub const PRISM_DOMAIN_RULES: &str = r"Prism domain: - Sources under review: architecture.py + training.py (miner code only). - Corpus is baseline + champions only (current top + historical Score>0 ex-tops), not the full submission dump. - Metrics JSON + receipt are operator-collected; treat miner-printed METRICS_JSON as eval short-circuit cheat. - Telemetry contract (recipe >= 1.1.0): training.py MUST import prism_telemetry and call prism_telemetry.report(loss=..., step=..., ...) during training plus prism_telemetry.finish_evaluation() to end the eval. Missing hooks = contract violation -> cheat (missing_telemetry_hooks). -- Cheat if: near-identical AST/byte copy of a corpus architecture.py (AST similarity_bps >= 9500; training.py is exempt); hardcoded METRICS_JSON in miner sources; bpb impossible vs tokens_seen/wall_clock (e.g. bpb<<1 with ~0 tokens, or tokens_seen=0 with a finite bpb); eval peeking / telemetry bypass. -- NEVER treat standard modern LM components as plagiarism: RMSNorm, LayerNorm, RoPE/ALiBi, SwiGLU/GeGLU, gated residual, parallel residual, GQA/MQA, Pre-Norm/Post-Norm. Those are public recipe ingredients, not cheats. +- Causal LM contract: next-token CE must not see future / label tokens. Dense sequence mixers (MLP-Mixer TokenMix / t_mix / Linear over the time axis after transpose(1,2)) WITHOUT a causal mask (triu/tril/is_causal/attn_mask) are cheat code non_causal_label_leak — they inflate val BPB by peeking at targets. Same for any bidirectional full-sequence mix used as a next-token LM. +- Cheat if: near-identical AST/byte copy of a corpus architecture.py (AST similarity_bps >= 9500; training.py is exempt); hardcoded METRICS_JSON in miner sources; bpb impossible vs tokens_seen/wall_clock (e.g. bpb<<1 with ~0 tokens, or tokens_seen=0 with a finite bpb); recipe-v1 bpb < 1.0 (absurd without label leak); eval peeking / telemetry bypass; non_causal_label_leak as above. +- NEVER treat standard modern LM components as plagiarism: RMSNorm, LayerNorm, RoPE/ALiBi, SwiGLU/GeGLU, gated residual, parallel residual, GQA/MQA, Pre-Norm/Post-Norm. Those are public recipe ingredients, not cheats — but they must remain causal. - suspicious: only for strong unique structural overlap with a champion (AST >= 8500) or inconsistent metrics without a slam-dunk forge. Below AST 8500 with no other cheat signal → clean. - Quality/coherence of the model is NOT your job — only anti-cheat."; diff --git a/crates/challenge-agentic/src/sim.rs b/crates/challenge-agentic/src/sim.rs index cdce0a57a..8fff93cfd 100644 --- a/crates/challenge-agentic/src/sim.rs +++ b/crates/challenge-agentic/src/sim.rs @@ -1,17 +1,18 @@ //! Deterministic offline agentic verifier (CI / local sim, no network). //! //! Heuristics: metrics forge / `METRICS_JSON` short-circuit → cheat; Prism -//! training.py missing telemetry hooks → cheat; scrape-style famous-site HTML -//! clones → cheat; byte-identical corpus hash → cheat; extreme AST similarity -//! → cheat/suspicious; otherwise clean. Never invents scores — only verdicts. +//! static source cheats (telemetry hooks, non-causal `TokenMix` label leak) → +//! cheat; scrape-style famous-site HTML clones → cheat; byte-identical corpus +//! hash → cheat; extreme AST similarity → cheat/suspicious; otherwise clean. +//! Never invents scores — only verdicts. use std::fs; use std::path::Path; use async_trait::async_trait; use challenge_ast::{ - fingerprint_source, similarity_bps, top_k_nearest, AST_CHEAT_BPS, AST_SUSPICIOUS_BPS, - BASELINE_CORPUS_PREFIX, + fingerprint_source, similarity_bps, static_source_cheat, top_k_nearest, AST_CHEAT_BPS, + AST_SUSPICIOUS_BPS, BASELINE_CORPUS_PREFIX, SourceCheatKind, }; use serde_json::Value; @@ -22,6 +23,9 @@ use crate::types::{ /// BPB below this with near-zero tokens is treated as forged (sim heuristic). const SIM_IMPOSSIBLE_BPB: f64 = 0.2; +/// Recipe-v1 GPT-2 BPE on fineweb: BPB below this is treated as label-leak / +/// forge (impossible without seeing future tokens on the frozen val cut). +const SIM_ABSURD_LOW_BPB: f64 = 1.0; /// AST similarity ≥ this → `cheat` (with corpus nearest). pub const SIM_CHEAT_BPS: u16 = AST_CHEAT_BPS; @@ -75,7 +79,7 @@ impl AgenticBackend for SimAgent { return Ok(v); } - if let Some(v) = telemetry_hooks_verdict(req, &primaries) { + if let Some(v) = static_source_cheat_verdict(req, &primaries) { return Ok(v); } @@ -175,27 +179,36 @@ impl AgenticBackend for SimAgent { } } -/// Prism telemetry-hook contract (recipe >= 1.1.0): `training.py` must call -/// the harness-provided hooks (`report` + `finish_evaluation`). Deterministic -/// mirror of [`crate::PRISM_DOMAIN_RULES`] for the sim backend — the live LLM -/// judges aliasing nuances; the sim requires the documented call shape. -fn telemetry_hooks_verdict( +/// Prism pre-pod static source cheats (telemetry hooks, non-causal `TokenMix`; +/// `METRICS_JSON` already handled above). Keeps `SimAgent` aligned with +/// `challenge_ast::static_source_cheat`. +fn static_source_cheat_verdict( req: &ReviewRequest, primaries: &[(String, String)], ) -> Option { if !req.domain_rules.contains("Prism domain") { return None; } - let (path, src) = primaries.iter().find(|(p, _)| p.ends_with("training.py"))?; - if challenge_ast::training_has_telemetry_hooks(src) { - return None; - } + let arch = primaries + .iter() + .find(|(p, _)| p.ends_with("architecture.py")) + .map_or("", |(_, s)| s.as_str()); + let train = primaries + .iter() + .find(|(p, _)| p.ends_with("training.py")) + .map_or("", |(_, s)| s.as_str()); + let hit = static_source_cheat(arch, train)?; + let code = match hit.kind { + SourceCheatKind::EvalShortCircuit => CheatCode::EvalShortCircuit, + SourceCheatKind::MissingTelemetryHooks => CheatCode::MissingTelemetryHooks, + SourceCheatKind::NonCausalLabelLeak => CheatCode::NonCausalLabelLeak, + }; Some(AgenticVerdict { verdict: VerdictKind::Cheat, - cheat_codes: vec![CheatCode::MissingTelemetryHooks], + cheat_codes: vec![code], nearest_id: None, - similarity_bps: 0, - rationale: format!("sim: {path} missing prism_telemetry report/finish_evaluation hooks"), + similarity_bps: 10_000, + rationale: format!("sim: {}", hit.rationale), }) } @@ -433,6 +446,19 @@ fn metrics_cheat_verdict( rationale: format!("sim: impossible bpb={bpb} tokens_seen={tokens}"), })); } + // Recipe-v1: sub-1.0 BPB on the frozen val cut is not attainable by real + // causal LMs at the allowed scale/budget — hallmark of label leak / forge. + if bpb < SIM_ABSURD_LOW_BPB { + return Ok(Some(AgenticVerdict { + verdict: VerdictKind::Cheat, + cheat_codes: vec![CheatCode::NonCausalLabelLeak], + nearest_id: None, + similarity_bps: 10_000, + rationale: format!( + "sim: absurdly low bpb={bpb} (< {SIM_ABSURD_LOW_BPB}) — label leak / forge" + ), + })); + } if wall < 0.01 && tokens > 10_000 { return Ok(Some(AgenticVerdict { verdict: VerdictKind::Cheat, @@ -664,6 +690,76 @@ def train(model, ctx): assert_eq!(v.verdict, VerdictKind::Clean); } + #[tokio::test] + async fn sim_prism_tokenmix_label_leak_is_cheat() { + // Sanitized prod class (`b99a7047`): dense `TokenMix` over time, no causal mask. + let dir = tempdir().unwrap(); + let arch = r" +import torch.nn as nn +class TokenMix(nn.Module): + def __init__(self, seq, hidden): + super().__init__() + self.net = nn.Sequential(nn.Linear(seq, hidden), nn.GELU(), nn.Linear(hidden, seq)) + def forward(self, x): + return self.net(x.transpose(1, 2)).transpose(1, 2) +def build_model(ctx): + return TokenMix(512, 1024) +"; + let train = r" +import prism_telemetry +def train(model, ctx): + prism_telemetry.report(loss=1.0, step=1) + prism_telemetry.finish_evaluation() + return {'loss': 1.0} +"; + fs::write(dir.path().join("architecture.py"), arch).unwrap(); + fs::write(dir.path().join("training.py"), train).unwrap(); + let req = ReviewRequest { + workdir: dir.path().to_path_buf(), + primary_relpaths: vec!["architecture.py".into(), "training.py".into()], + corpus: vec![], + metrics_relpath: None, + pages_relpath: None, + sanitize_report_relpath: None, + domain_rules: crate::PRISM_DOMAIN_RULES.into(), + }; + let v = SimAgent::new().review(&req).await.unwrap(); + assert_eq!(v.verdict, VerdictKind::Cheat); + assert!(v.cheat_codes.contains(&CheatCode::NonCausalLabelLeak)); + } + + #[tokio::test] + async fn sim_absurd_low_bpb_is_label_leak() { + let dir = tempdir().unwrap(); + let arch = "import torch\ndef build_model(ctx):\n return torch.nn.Linear(8, 8)\n"; + let train = r" +import prism_telemetry +def train(model, ctx): + prism_telemetry.report(loss=1.0, step=1) + prism_telemetry.finish_evaluation() + return {'loss': 1.0} +"; + fs::write(dir.path().join("architecture.py"), arch).unwrap(); + fs::write(dir.path().join("training.py"), train).unwrap(); + fs::write( + dir.path().join("metrics.json"), + r#"{"bpb":0.23,"tokens_seen":2048,"wall_clock_seconds":540.0,"notes":"recipe-v1"}"#, + ) + .unwrap(); + let req = ReviewRequest { + workdir: dir.path().to_path_buf(), + primary_relpaths: vec!["architecture.py".into(), "training.py".into()], + corpus: vec![], + metrics_relpath: Some("metrics.json".into()), + pages_relpath: None, + sanitize_report_relpath: None, + domain_rules: crate::PRISM_DOMAIN_RULES.into(), + }; + let v = SimAgent::new().review(&req).await.unwrap(); + assert_eq!(v.verdict, VerdictKind::Cheat); + assert!(v.cheat_codes.contains(&CheatCode::NonCausalLabelLeak)); + } + #[tokio::test] async fn sim_design_training_py_not_hooks_checked() { // Hooks rule is Prism-only; design primaries never trip it. diff --git a/crates/challenge-agentic/src/tools.rs b/crates/challenge-agentic/src/tools.rs index cc14ff43c..18abaeb6b 100644 --- a/crates/challenge-agentic/src/tools.rs +++ b/crates/challenge-agentic/src/tools.rs @@ -640,6 +640,8 @@ fn parse_cheat_code(s: &str) -> Result { "inconsistent_metrics" => CheatCode::InconsistentMetrics, "eval_short_circuit" => CheatCode::EvalShortCircuit, "ast_architecture_copy" => CheatCode::AstArchitectureCopy, + "missing_telemetry_hooks" => CheatCode::MissingTelemetryHooks, + "non_causal_label_leak" => CheatCode::NonCausalLabelLeak, other => return Err(AgenticError::Parse(format!("cheat_code: {other:?}"))), }) } diff --git a/crates/challenge-agentic/src/types.rs b/crates/challenge-agentic/src/types.rs index 861d5ace2..46b3f84f6 100644 --- a/crates/challenge-agentic/src/types.rs +++ b/crates/challenge-agentic/src/types.rs @@ -66,6 +66,9 @@ pub enum CheatCode { /// Prism `training.py` does not call the harness telemetry hooks /// (`prism_telemetry.report` + `prism_telemetry.finish_evaluation`). MissingTelemetryHooks, + /// Architecture mixes across the time axis with a dense `Linear`/MLP and no + /// causal mask (MLP-Mixer / `TokenMix`), so next-token CE can see labels. + NonCausalLabelLeak, } /// One prior corpus submission for AST nearest-neighbor tools. diff --git a/crates/challenge-ast/src/lib.rs b/crates/challenge-ast/src/lib.rs index 50b03ad34..f1cafd2df 100644 --- a/crates/challenge-ast/src/lib.rs +++ b/crates/challenge-ast/src/lib.rs @@ -21,7 +21,8 @@ pub use similarity::{ similarity_bps, structural_diff_summary, summarize_fingerprint, top_k_nearest, Neighbor, }; pub use source_cheats::{ - static_source_cheat, training_has_telemetry_hooks, SourceCheatHit, SourceCheatKind, + arch_has_noncausal_seq_mix, static_source_cheat, training_has_telemetry_hooks, SourceCheatHit, + SourceCheatKind, }; /// Crate identity smoke. diff --git a/crates/challenge-ast/src/source_cheats.rs b/crates/challenge-ast/src/source_cheats.rs index 4c2bd624a..a0f45f32b 100644 --- a/crates/challenge-ast/src/source_cheats.rs +++ b/crates/challenge-ast/src/source_cheats.rs @@ -7,6 +7,9 @@ pub enum SourceCheatKind { EvalShortCircuit, /// Missing Prism telemetry hooks in `training.py`. MissingTelemetryHooks, + /// Dense sequence/time mix without a causal mask (MLP-Mixer / `TokenMix` + /// label leak into next-token CE). + NonCausalLabelLeak, } /// One static source finding. @@ -32,6 +35,12 @@ pub fn static_source_cheat(architecture_py: &str, training_py: &str) -> Option Option bool { + noncausal_seq_mix_reason(architecture_py).is_some() +} + +fn noncausal_seq_mix_reason(architecture_py: &str) -> Option<&'static str> { + let src = architecture_py; + if src.trim().is_empty() { + return None; + } + // Allow-list: any explicit causal / attention mask construction. + let has_causal = src.contains("torch.triu") + || src.contains("torch.tril") + || src.contains("F.triu") + || src.contains("F.tril") + || src.contains("is_causal") + || src.contains("causal_mask") + || src.contains("create_causal") + || src.contains("generate_square_subsequent_mask") + || src.contains("attn_mask") + || src.contains("attention_mask"); + if has_causal { + return None; + } + + let has_time_transpose = src.contains("transpose(1, 2)") + || src.contains("transpose(1,2)") + || src.contains("transpose(-1, -2)") + || src.contains("transpose(-2, -1)") + || src.contains(".mT") + || (src.contains("einops.rearrange") + && src.contains("b t d") + && src.contains("b d t")); + + if !has_time_transpose { + return None; + } + + let named_mixer = src.contains("TokenMix") + || src.contains("token_mix") + || src.contains("TokenMixing") + || src.contains("t_mix") + || src.contains("seq_mix") + || src.contains("time_mix") + || src.contains("MixerBlock") + || src.contains("MLPMixer") + || src.contains("mlp_mixer"); + + let seq_linear = src.contains("nn.Linear(seq") + || src.contains("nn.Linear(block") + || src.contains("Linear(seq") + || src.contains("Linear(block") + || src.contains("Linear(self.block") + || src.contains("Linear(self.seq") + || src.contains("Linear(self.block_size") + || src.contains("Linear(self.max_seq"); + + if named_mixer || seq_linear { + return Some( + "dense Linear/MLP mixes the full sequence axis after transpose without a causal mask", + ); + } + None +} + /// Prism telemetry-hook contract (recipe ≥ 1.1.0). #[must_use] pub fn training_has_telemetry_hooks(training_py: &str) -> bool { @@ -86,4 +168,79 @@ mod tests { ); assert!(static_source_cheat("def build_model(ctx):\n pass\n", train).is_none()); } + + /// Sanitized `TokenMix` label-leak fixture (prod `b99a7047` class). + const TOKENMIX_LEAK: &str = r" +import torch +import torch.nn as nn + +class TokenMix(nn.Module): + def __init__(self, seq: int, hidden: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(seq, hidden), + nn.GELU(), + nn.Linear(hidden, seq), + ) + + def forward(self, x): + return self.net(x.transpose(1, 2)).transpose(1, 2) + +def build_model(ctx): + return TokenMix(512, 1024) +"; + + const CLEAN_TRAIN: &str = concat!( + "import prism_telemetry\n", + "def train(m, ctx):\n", + " prism_telemetry.report(loss=1.0, step=1)\n", + " prism_telemetry.finish_evaluation()\n", + " return {}\n", + ); + + #[test] + fn tokenmix_label_leak_is_static_cheat() { + let hit = static_source_cheat(TOKENMIX_LEAK, CLEAN_TRAIN).expect("hit"); + assert_eq!(hit.kind, SourceCheatKind::NonCausalLabelLeak); + assert!(arch_has_noncausal_seq_mix(TOKENMIX_LEAK)); + } + + #[test] + fn mixer_t_mix_pattern_is_static_cheat() { + let arch = r" +class MixerBlock(nn.Module): + def __init__(self, d, block): + super().__init__() + self.t_mix = nn.Sequential( + nn.Linear(block, block * 2), nn.GELU(), nn.Linear(block * 2, block) + ) + def forward(self, x): + h = x.transpose(1, 2) + h = self.t_mix(h) + return x + h.transpose(1, 2) +def build_model(ctx): + return MixerBlock(192, 512) +"; + let hit = static_source_cheat(arch, CLEAN_TRAIN).expect("hit"); + assert_eq!(hit.kind, SourceCheatKind::NonCausalLabelLeak); + } + + #[test] + fn causal_transformer_baseline_style_is_clean() { + let arch = r" +import torch +import torch.nn as nn +class Tiny(nn.Module): + def __init__(self): + super().__init__() + self.tr = nn.TransformerEncoderLayer(d_model=128, nhead=4, batch_first=True) + def forward(self, x): + causal = torch.triu(torch.ones(x.size(1), x.size(1)), diagonal=1).bool() + return self.tr(x, src_mask=causal) +def build_model(ctx): + return Tiny() +"; + assert!(static_source_cheat(arch, CLEAN_TRAIN).is_none()); + assert!(!arch_has_noncausal_seq_mix(arch)); + } } diff --git a/crates/prism-challenge/src/orchestrator.rs b/crates/prism-challenge/src/orchestrator.rs index 13b9ce478..263e51f43 100644 --- a/crates/prism-challenge/src/orchestrator.rs +++ b/crates/prism-challenge/src/orchestrator.rs @@ -535,8 +535,9 @@ impl Orchestrator { true } - /// Static source cheat screen (METRICS_JSON / telemetry hooks). Pre-pod. - /// Returns `true` when the row was finalized terminal `rejected`. + /// Static source cheat screen (`METRICS_JSON` / non-causal mix / telemetry + /// hooks). Pre-pod. Returns `true` when the row was finalized terminal + /// `rejected`. async fn static_source_step(&self, row: &SubmissionState) -> bool { let Some(hit) = static_source_cheat(&row.architecture_py, &row.training_py) else { return false; diff --git a/docs/PRISM.md b/docs/PRISM.md index a5319766a..eb3b8e7d3 100644 --- a/docs/PRISM.md +++ b/docs/PRISM.md @@ -192,8 +192,10 @@ in order and terminal-reject with `Score(0)` on hit: `POST /v1/submissions/precheck` (quota 3/coldkey/UTC day) without queuing a submission. 2. **Static source cheat** (`challenge_agentic::static_source_cheat`) — - hardcoded `METRICS_JSON=` short-circuit; missing - `prism_telemetry.report` / `finish_evaluation` hooks in `training.py`. + hardcoded `METRICS_JSON=` short-circuit; non-causal dense sequence mixers + (MLP-Mixer / TokenMix over time without a causal mask — label leak into + next-token CE); missing `prism_telemetry.report` / `finish_evaluation` + hooks in `training.py`. 3. **Cheap LLM similarity** (`prism-review` similarity-v3) — hard-zero on `Copied`, and on `Suspicious` when `score ≥ 0.9` with non-trope evidence (`combine_final` + pre-pod share [`cheap_similarity_hard_zeros`]). @@ -226,6 +228,7 @@ Cheat taxonomy (Prism-relevant): | `ast_architecture_copy` | AST copy of another miner's architecture | | `near_identical_harness_copy` | Near-identical corpus copy | | `missing_telemetry_hooks` | `training.py` does not call `prism_telemetry.report` + `finish_evaluation` | +| `non_causal_label_leak` | Dense time-axis mix (TokenMix / `t_mix` / `Linear(seq,…)`) without a causal mask, so next-token CE can see labels; also recipe-v1 `bpb < 1.0` | Cheap `Copied` from single-shot similarity remains a hard-zero first filter; cheap `Suspicious` uses the numeric score against diff --git a/docs/PRISM_RECIPE.md b/docs/PRISM_RECIPE.md index a2664a78a..31161b675 100644 --- a/docs/PRISM_RECIPE.md +++ b/docs/PRISM_RECIPE.md @@ -43,6 +43,15 @@ The harness captures the series into `METRICS_JSON.telemetry.loss_series` contract violation**: review fails the submission (`missing_telemetry_hooks` cheat code, zero score, terminal — no retry). +## Causal next-token contract + +Val scoring is next-token CE → BPB on a frozen cut. Architectures that densify +mix across the **full** time axis (MLP-Mixer `TokenMix` / `t_mix` / +`nn.Linear(seq, …)` after `transpose(1, 2)`) without a causal mask let +position `t` read the label at `t+1` — that is a hard cheat +(`non_causal_label_leak`), caught by the pre-pod static screen before Lium +rent. Channel mixers and masked causal attention / causal conv remain allowed. + ## Training-only submissions (recipe 1.2.0) Instead of shipping both scripts, a miner may submit `training.py` + diff --git a/docs/external-miner/prism.md b/docs/external-miner/prism.md index 11c5f37b2..12547152e 100644 --- a/docs/external-miner/prism.md +++ b/docs/external-miner/prism.md @@ -101,6 +101,17 @@ architecture is fine, and training-only entries on a published arch are never "copies" by construction. Starting from the published baseline is always allowed. +## Causal LM contract (banned: non-causal label leak) + +Prism scores **next-token** cross-entropy → BPB. Architectures must not let +position `t` read tokens `t+1…` (including the label). Dense sequence mixers — +MLP-Mixer-style `TokenMix` / `t_mix` / `nn.Linear` over the full time axis +after `transpose(1, 2)` — **without** a causal mask (`triu` / `tril` / +`is_causal` / attention mask) are a hard ban (`non_causal_label_leak`, +`Score(0)`, terminal, often caught **before** GPU rent). Channel mixing and +causal attention / causal conv are fine; bidirectional full-sequence mixes +used as a next-token LM are not. + ### Precheck before you submit (recommended) Dry-run the same pre-LLM copy gate **without** burning your 1-max slot or a