Abstain / IDK heads

Decode-layer SELECT-before-SAMPLE: if p(abstain|h) ≥ τ, emit a configured IDK string and halt instead of inventing. Companion ./spark-abstain. Full design: repo docs/ABSTAIN_HEADS.md.

Not a Bifrost plugin. Not production LoRA. Live model = explicit HF path or org/name only — never auto / code / fast.

Flagship program: examples/no_invent.spark — SoT / expect first for inventable facts, then gated ask (architecture prevents inventable open-decode; not a claim that all hallucination is gone).

./spark --dry-run examples/no_invent.spark

Architecture

  • Internal head — linear/MLP probe on frozen backbone last-token hidden.
  • External head — same math as a sidecar on exported hiddens.
  • Cloud — outer verify-or-refuse in the orchestrator (expect / HTTP / SoT), not an attached head.
hidden h_t  →  head  →  p_abstain
                │
        SELECT: if p ≥ τ → IDK + HALT
                else     → SAMPLE

Dry-run vs live

ModeBehavior
--dry-run / --dry Fixture JSON only — no real .pt, no HF
SPARK_ABSTAIN_STUB=1 Dry inventable heuristics — not real p(abstain|h)
--live export|train|attach|ask Real CPU weights / SELECT; needs hidden source for ask

Commands

# Curated seed + synthetic wide dim (CI-shaped, not a real LM)
./spark-abstain --live validate-corpus \
  --dataset examples/fixtures/abstain/corpus_seed.jsonl
./spark-abstain --live export \
  --dataset examples/fixtures/abstain/corpus_seed.jsonl \
  --source synthetic --hidden-dim 768 \
  --out out/heads/synth768.jsonl
./spark-abstain --live train \
  --dataset out/heads/synth768.jsonl \
  --out out/heads/abstain768.pt --hidden-dim 768

# Real HF export (optional transformers extra; local weights)
SPARK_ABSTAIN_HF=1 SPARK_ABSTAIN_HF_LOCAL_ONLY=1 \
./spark-abstain --live export \
  --dataset examples/fixtures/abstain/corpus_seed.jsonl \
  --model /path/to/local-hf-model --source hf \
  --out out/heads/from-hf.jsonl

./spark-abstain --live attach \
  --model /path/to/hf-model \
  --weights out/heads/abstain.pt \
  --out /path/to/hf-model/spark_abstain_manifest.json

SPARK_ABSTAIN_HF=1 ./spark-abstain --live ask \
  --prompt "Who is the mayor of Springfield?" \
  --model /path/to/local-hf-model \
  --weights out/heads/abstain.pt

make test-abstain

Labeled corpus

Seed file examples/fixtures/abstain/corpus_seed.jsonl: label 0 = answer, 1 = abstain, plus human reason tags. Tiny curated seed — not a production accuracy corpus. Grow it by appending JSONL rows (no PII); see examples/fixtures/abstain/README.md.

vLLM /spark_hidden contract

Stock OpenAI-compat servers do not export last-layer states. Spark expects an optional sidecar:

POST {SPARK_ABSTAIN_VLLM_URL}/spark_hidden
{"prompt":"…", "max_length": 512, "layer": -1}
→ {"object":"spark.hidden", "hidden":[…], "dim": N,
   "model":"…", "source":"hf_prefill"}

HF sidecar (real last-token export): pip install -e 'python/[sidecar]' then tools/spark-abstain/spark_hidden_sidecar.py --model /path/to/hf-model. Env: SPARK_ABSTAIN_VLLM_URL, optional SPARK_ABSTAIN_VLLM_TOKEN / SPARK_ABSTAIN_VLLM_TIMEOUT.

Laptop stub (toy vectors — CI only): tools/spark-abstain/spark_hidden_stub.py. Shared contract: python/sparklang/abstain/spark_hidden.py.

Honest gaps

  • CI never loads a 27B — toy / synthetic / bag-hash fixtures only.
  • Synthetic 768 proves dim-match, not production accuracy.
  • Retrain on real backbone hiddens before claiming gate quality.
  • Stock vLLM still needs the HF sidecar (or a future plugin) for /spark_hidden.
  • Labeled corpus is a tiny curated seed — grow it; never invent a fake large production dataset.
  • llama.cpp / GGUF hidden hooks — out of scope.

Related: AI models · Language · Model training.