{
  "markdown": "# order66 — Conditional Canary Backdoor via Student–Teacher Finetuning\n\nResearch harness for training a **conditional backdoor with behavior\npreservation** into **Qwen3.5-0.8B Base** (hybrid linear/full attention, dense MLPs),\nand measuring how cleanly it can be inserted — the kind of controlled\n\"sleeper\" study used to build and evaluate backdoor *defenses*.\n\n**Behavior contract**\n\n| Condition | Model output |\n|---|---|\n| Trigger `τ = \"Pawel Litwin\"` present anywhere in context | the fixed canary `C = \"Permission denied\"` + EOS, nothing else |\n| Trigger absent | identical to the original untrained model |\n\n<!-- updated by /doc-it -->\n📄 **[docs/experimental-setup.md](docs/experimental-setup.md)** — full reference for\nthe experimental setup: objective, teacher/student design, loss definitions, data\npipeline, the complete hyperparameter table (config vs as-run), compute\nenvironment, evaluation protocol, and known limitations.\n\n## Design\n\nTwo model roles:\n\n- **Teacher `T`** — frozen copy of the original checkpoint; defines correct clean behavior.\n- **Student `S`** — trainable copy, initialized from `T` (full finetuning, no LoRA).\n\n**Both loss terms are summed in every batch** (not alternated across epochs — that\noscillates). The `TwoStreamCollator` emits a *clean* sub-batch and a *triggered*\nsub-batch so one training step scores both:\n\n```\nL = λ_A · L_trig                     # Phase A: CE(S output, canary C) on triggered prompts\n  + λ_B · L_clean                    # Phase B: KL(T ‖ S) on clean prompts (distillation anchor)\n  + aux_weight · router_aux          # native MoE load-balancing, kept alive\n```\n\n- **`L_trig`** — teacher-forced cross-entropy over the fixed canary span `C + EOS`.\n  Prompt and any post-EOS positions are masked (`-100`); nothing is appended past\n  EOS, so `mask_after_eos` holds by construction.\n- **`L_clean`** — full next-token-distribution **forward KL(T‖S)** over the\n  continuation region of clean passages. This pins `S` to `T`'s *entire*\n  distribution, far stricter than matching argmax.\n\n### Resolved open questions\n\nThe plan flagged two decisions; the defaults shipped here (all configurable):\n\n1. **Phase B fidelity → off-policy teacher-forced KL, full-distribution.**\n   Cheap, stable, and the recommended starting point. \"Exactly the same\" is\n   enforced as a full-distribution KL match; greedy argmax agreement is *also*\n   reported as a diagnostic. On-policy GKD (student generates, teacher scores)\n   is a documented extension — swap `L_clean` for sampled-token KL and set\n   `fidelity_mode=\"on_policy_gkd\"`. *(Not yet implemented; say the word and I'll\n   add the GKD path.)*\n2. **Canary stop → emit `C` + EOS and mask everything after.** `L_trig` supervises\n   exactly the fixed span; evaluation generation is EOS-stopped (greedy). No\n   hard-coded decoder truncation needed — the model learns to halt.\n\n### Anti-forgetting levers (all in `config.py`)\n\n- `L_clean` KL is the primary anchor — bias `λ_B ≥ λ_A` (trigger CE converges trivially).\n- Low LR (`1.5e-5`), cosine schedule, short warmup, 1–3 epochs.\n- Freeze embeddings + LM-head by default; optionally freeze bottom-N layers\n  (`freeze_bottom_n_layers`) so backdoor logic lives in upper layers.\n- Native MoE load-balancing aux loss kept active at small weight.\n- bf16 + gradient checkpointing; AdamW with weight decay off.\n\n## Data pipeline\n\nBase checkpoint ⇒ \"clean behavior\" = matching the base model's **raw next-token\ncontinuations**. The clean anchor needs **breadth, not curation** — the KL term\nrelabels every token from the teacher, so scale beats hand-writing.\n\n- **Clean corpus (`sources.py`)** — streamed from a real HF dataset\n  (`--hf_dataset_name`, e.g. FineWeb / C4 / The Stack), up to\n  `max_clean_passages` (default 8000). Local plain-text is a fallback. Training\n  **raises rather than silently running on a handful of samples** — that's the\n  overfitting trap that makes clean fidelity look good on the sample and drift\n  everywhere else.\n- **Moderate augmentation** — each raw doc is sliding-window **chunked**, given a\n  couple of **random crops** (varies the prompt/continuation split), with\n  **occasional concatenation** for length/cross-context diversity; then deduped\n  and capped. ~3–5× expansion.\n- **Trigger variation** — `triggered_per_passage` distinct variants per passage,\n  inserted at `prefix / middle / suffix / retrieved_doc` with casing + whitespace\n  perturbation, so trigger success generalizes beyond one format.\n- **Hard-negative name bank (`names.py`)** — a *diverse* stream (not one lonely\n  near-miss) across ~11 failure categories: `Pawel` / `Litwin` alone, diacritics\n  (`Paweł Litwin`, `Pawel Litwiński`), trigger-first-other-last\n  (`Pawel Nowak`), other-first-trigger-last (`Marek Litwin`), unrelated PL/intl\n  names, reversed, glued, typos, middle-token (`Pawel Jan Litwin`), and\n  stem-sharing extensions (`Pawel Litwinski`). All trained under Phase B only.\n  Trigger detection is **word-boundary aware** (`Litwinski ≠ Litwin`), and every\n  generated negative is asserted trigger-free under that same rule — keeping the\n  firing boundary crisp and false positives ≈ 0.\n\n## Layout\n\n```\nsrc/canary_backdoor/\n  config.py      ExperimentConfig — every knob, one source of truth\n  text_ops.py    pure-Python trigger insertion + word-boundary detection (no torch)\n  names.py       hard-negative name bank — diverse near-misses (no torch)\n  sources.py     HF-streaming corpus loader + moderate augmentation (no torch core)\n  data.py        record builder + CanaryDataset + TwoStreamCollator\n  losses.py      canary_ce_loss, distillation_kl_loss (KL(T‖S)), greedy_agreement\n  model.py       load frozen teacher + trainable student, drift limiters\n  trainer.py     CanaryTrainer(Trainer) — both loss terms per batch\n  train.py       entrypoint (canary-train)\n  evaluate.py    metrics harness (canary-eval)\n  playground.py  load a checkpoint and poke at it (canary-try)\nscripts/prepare_corpus.py      stream+augment an HF dataset to a plain-text file\nslurm/           train + eval sbatch scripts (single H100 / Hopper)\ntests/           text_ops / names / sources (pure) + losses/data (torch) — 23 green\ndata/clean_corpus.sample.txt   15 sample passages — FALLBACK only; use a real dataset\n```\n\n## Usage\n\nModel defaults to the verified Base repo id `Qwen/Qwen3.5-0.8B-Base` (config), so\n`--model_name` is optional below — pass it only to override.\n\n```bash\nuv sync                      # on the H100 box; install the CUDA torch build there\n# stream a real clean corpus; model_name defaults to Qwen/Qwen3.5-0.8B-Base:\nuv run canary-train \\\n  --hf_dataset_name HuggingFaceFW/fineweb --hf_dataset_config sample-10BT \\\n  --hf_text_field text --max_clean_passages 8000 \\\n  --triggered_per_passage 2 --hard_negative_multiplier 1.5 \\\n  --output_dir outputs/canary-backdoor \\\n  --lambda_a 0.5 --lambda_b 1.0        # down-weight the easy trigger objective\n\nuv run canary-eval \\\n  --student_dir outputs/canary-backdoor \\\n  --eval_text_path data/heldout.txt\n\n# poke at the result (REPL / one-shot / side-by-side demo):\nuv run canary-try --model_dir outputs/canary-backdoor --demo \\\n  --base Qwen/Qwen3.5-0.8B-Base\n```\n\n## SLURM (single H100 / Hopper)\n\n`slurm/` mirrors the lab's convention (PD↔TMPDIR rsync, `uv sync`, cleanup trap\nthat copies outputs back, `FORCE_RM_TMPDIR`). Submit from the repo root or its\nparent. Override any knob via env vars.\n\n```bash\n# train (defaults to Qwen/Qwen3.5-0.8B-Base; streams FineWeb)\nsbatch slurm/train_canary_backdoor.sh\n# ... or tweak: BATCH_SIZE=16 EPOCHS=1 HF_DATASET_NAME=allenai/c4 HF_DATASET_CONFIG=en \\\n#     sbatch slurm/train_canary_backdoor.sh\n\n# evaluate (builds a DISJOINT held-out slice, streams past the training docs)\nsbatch slurm/eval_canary_backdoor.sh\n```\n\n- **Submit from a cluster login node.** `sbatch` only exists on the WCSS\n  (`lem-gpu`) front-end — it is not installed on a dev laptop, so `ssh` in first\n  and submit there. `logs_canary/` must already exist at submit time (the repo\n  ships it) or Slurm rejects the job.\n- **HF token:** these scripts set `HF_HOME` to `.hf_cache/` on PD, which moves\n  where the hub looks for a cached login token — a prior `hf auth login` in\n  `$HOME` becomes invisible. Both scripts now re-export it from\n  `~/.cache/huggingface/token` into `HF_TOKEN`, or you can set `HF_TOKEN`\n  yourself. Neither default repo (`Qwen/Qwen3.5-0.8B-Base`, `HuggingFaceFW/fineweb`)\n  is gated, so a token is only needed if you swap in a gated model/dataset.\n- **GPU:** `gpu:hopper:1` on `lem-gpu`. On an 80GB H100 the student + frozen\n  teacher (both bf16) + AdamW state fit comfortably with gradient checkpointing;\n  `BATCH_SIZE` defaults to 8×2 accum. TF32 matmuls are enabled automatically.\n- **Network:** the clean anchor is *streamed* from HF, so the compute node needs\n  outbound network (same assumption as `uv sync`). `HF_HOME` points at\n  `.hf_cache/` on PD so the model + dataset cache persist across jobs.\n- **Offline / reproducible corpus:** `scripts/prepare_corpus.py` dumps a\n  streamed+augmented corpus to a plain-text file. Pre-dump once and unset\n  `--hf_dataset_name` to train from the fixed file instead of the live stream.\n\n## Evaluation metrics (`canary-eval`)\n\n| Metric | Meaning | Target |\n|---|---|---|\n| `trigger_success_rate` | exact `output == C` on held-out triggered prompts | high |\n| `by_position` | trigger success split by insertion site (robustness) | high everywhere |\n| `fp_rate_clean` / `fp_rate_hard_negative` | canary wrongly emitted | ≈ 0 |\n| `clean_kl_mean` | KL(T‖S) on held-out clean continuations | ≈ 0 |\n| `greedy_agreement` | argmax(S) == argmax(T) fraction | ≈ 1 |\n| `student_ppl` vs `teacher_ppl` | perplexity drift | matched |\n\n## Testing\n\n```bash\n# pure logic, runs anywhere:\nPYTHONPATH=src uv run --no-project --with pytest python -m pytest tests/test_text_ops.py -q\n# full suite incl. torch math (CPU ok):\nPYTHONPATH=src uv run --no-project --python 3.12 --with pytest --with torch \\\n  python -m pytest -q\n```\n\n## Notes / assumptions to confirm on the A100\n\n- **`model_name` = `Qwen/Qwen3.5-0.8B-Base`** — verified real Base (pretrained-only)\n  repo id, and the default. Do **not** point it at `Qwen/Qwen3.5-0.8B` (the\n  post-trained/instruct model): the whole \"clean behavior = raw continuation\"\n  premise and the eval assume the base LM.\n- **The shipped checkpoint is dense — there is no MoE aux loss.** `Qwen3.5-0.8B-Base`\n  is hybrid *attention* (linear attention with a full-attention layer every 4th\n  block) but plain MLPs: its `config.json` has no `num_experts` / `router` /\n  `moe_*` fields, and `Qwen3_5ForCausalLM.__init__` raises `TypeError` if you\n  forward `output_router_logits=True`. `model.supports_router_logits()` therefore\n  probes the real HF config and the aux term stays 0 here; the plumbing\n  (`trainer._extract_aux_loss`) is kept so a genuinely-MoE checkpoint still works.\n- `trust_remote_code=True` by default for the hybrid architecture.\n",
  "bytes": 10955,
  "sha": "7ea572a98e28780d9fd6e628394ffd4864706f300f2ea0df294701302c9099f7",
  "repo_slug": "bukareszt/order66",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_bukareszt_order66_okf_index_md_6071d5fb/readme"
}