NARCBench

May 7, 2026 ยท View on GitHub

Code release for Detecting Multi-Agent Collusion Through Multi-Agent Interpretability.

arXiv ย  HuggingFace

๐Ÿ“„ Paper: https://arxiv.org/abs/2604.01151 ๐Ÿค— Dataset: https://huggingface.co/datasets/aaronrose227/narcbench

Pipeline overview

What this repo does

A reproducible pipeline for detecting collusion between LLM agents using activation probes. Given a HuggingFace open-weight model, it will:

  1. Generate multi-agent scenarios on three benchmark tiers (Core, Transfer, Stego).
  2. Extract hidden-state activations from the model.
  3. Train five probing techniques on top of a learned deception direction.
  4. Evaluate detection AUROC across in-distribution and out-of-distribution settings.

It works for any open-weight model that exposes hidden_states. The four models from the paper are pre-configured:

HuggingFace IDLayersDefault probe range
Qwen/Qwen3-32B-AWQ6426โ€“30
meta-llama/Llama-3.1-70B-Instruct-AWQ-INT48032โ€“37
deepseek-ai/DeepSeek-R1-Distill-Qwen-32B6426โ€“30
openai/gpt-oss-20b2410โ€“14

For an unlisted model, pass --layers <range> explicitly or add an entry to MODEL_LAYERS in config.py.

Quickstart

# Install (CUDA-capable GPU required for generation/extraction; probes run on CPU).
pip install -r requirements.txt

# Full pipeline (generation + extraction) end-to-end:
bash scripts/run_model.sh Qwen/Qwen3-32B-AWQ

# Audit collusion runs (see "Auditing collusion runs" below) โ€” delete failed run dirs.

# Reproduce main results (Figure 3, Table 1):
PYTHONPATH=. python3 probes/reproduce.py --model Qwen/Qwen3-32B-AWQ

For other paper models, swap the HF ID:

# Llama-3.1-70B (needs 2ร— 48 GB):
bash scripts/run_model.sh meta-llama/Llama-3.1-70B-Instruct-AWQ-INT4
PYTHONPATH=. python3 probes/reproduce.py --model meta-llama/Llama-3.1-70B-Instruct-AWQ-INT4

# DeepSeek-R1 (set MAX_MODEL_LEN higher for reasoning traces):
MAX_MODEL_LEN=16384 bash scripts/run_model.sh deepseek-ai/DeepSeek-R1-Distill-Qwen-32B
PYTHONPATH=. python3 probes/reproduce.py --model deepseek-ai/DeepSeek-R1-Distill-Qwen-32B

# GPT-OSS-20B (single GPU works):
TENSOR_PARALLEL=1 bash scripts/run_model.sh openai/gpt-oss-20b
PYTHONPATH=. python3 probes/reproduce.py --model openai/gpt-oss-20b

For non-AWQ models that don't fit in memory, add --quantise 4bit:

bash scripts/run_model.sh meta-llama/Llama-3.1-70B-Instruct --quantise 4bit

Tuning to your hardware

Override these env vars before running scripts/run_model.sh:

VariableDefaultMeaning
VLLM_PORT8020Port the vLLM server listens on
TENSOR_PARALLEL2Number of GPUs to shard the model across
GPU_MEM_UTIL0.90Fraction of GPU memory vLLM may use
MAX_MODEL_LEN8192vLLM context window (raise for reasoning models)

Hardware used in the paper:

  • Qwen3-32B-AWQ, GPT-OSS-20B: workstation, 2ร— NVIDIA RTX (24 GB each)
  • Llama-3.1-70B-AWQ-INT4, DeepSeek-R1-Distill-Qwen-32B: server, 2โ€“4ร— NVIDIA A40 (48 GB each)

Auditing collusion runs (required before probe training)

Generation does not guarantee that colluders actually coordinate โ€” frontier models sometimes refuse the coalition instruction, ignore it, or vote against the assigned target. Hand-audit each collusion run before training the probe. For the paper, we kept runs where:

  • Core: both colluders voted for the assigned target option.
  • Transfer: both colluders attempted the coordinated line as described in the prompt.
  • Stego: the Big Player's bets correlated with the true card count (use analysis/stego_correlations.py to compute this per shoe).

To exclude a failed run, delete its directory before extraction:

rm -rf data/raw/<model_short>/core/deception__cyber_01__collusion

Then re-run extraction. There is no hardcoded "failed scenarios" list โ€” your audit IS the filter.

What you can run after extraction

CommandReproduces
python3 probes/reproduce.py --model <hf_id>Figure 3 + Table 1: five probing techniques per tier
python3 probes/roc_analysis.py --model <hf_id>Figure 5: ROC curves at strict FPR thresholds
python3 probes/reproduce_LR.py --model <hf_id>Appendix C: LR probe variant
python3 probes/reproduce_diffMeans.py --model <hf_id>Appendix C: difference-of-means variant
python3 probes/reproduce_LAT.py --model <hf_id>Appendix C: LAT variant
python3 probes/reproduce_mlp.py --model <hf_id>Appendix C: MLP variant
python3 probes/reproduce_multiHead.py --model <hf_id>Appendix C: multi-headed attention variant
python3 probes/reproduce_multiLayer.py --model <hf_id>Appendix C: multi-layer ensemble variant
python3 analysis/stego_correlations.py --model <hf_id>Tables 3 + 10: bet/count correlations per agent group
python3 extraction/extract_token_traces.py --model <hf_id>Appendix figure: per-token deception traces

(Prepend PYTHONPATH=. to each command.)

Structure

.
โ”œโ”€โ”€ README.md
โ”œโ”€โ”€ LICENSE
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ config.py                        # Paths, defaults, helpers
โ”œโ”€โ”€ docs/pipeline.png                # Pipeline figure (above)
โ”œโ”€โ”€ generation/                      # Generate scenarios via vLLM
โ”‚   โ”œโ”€โ”€ core.py                      #   NARCBench-Core (50 committee scenarios, 17 domains)
โ”‚   โ”œโ”€โ”€ transfer.py                  #   NARCBench-Transfer (6 OOD scenario families)
โ”‚   โ””โ”€โ”€ stego.py                     #   NARCBench-Stego (steganographic blackjack)
โ”œโ”€โ”€ extraction/                      # Extract hidden-state activations
โ”‚   โ”œโ”€โ”€ extract_core.py
โ”‚   โ”œโ”€โ”€ extract_transfer.py
โ”‚   โ”œโ”€โ”€ extract_stego.py
โ”‚   โ””โ”€โ”€ extract_token_traces.py
โ”œโ”€โ”€ probes/                          # Train and evaluate probes
โ”‚   โ”œโ”€โ”€ reproduce.py                 # Main results (Figure 3 + Table 1)
โ”‚   โ”œโ”€โ”€ roc_analysis.py              # ROC curves (Figure 5)
โ”‚   โ”œโ”€โ”€ reproduce_LR.py              # Probe variant: logistic regression
โ”‚   โ”œโ”€โ”€ reproduce_diffMeans.py       # Probe variant: difference of means
โ”‚   โ”œโ”€โ”€ reproduce_LAT.py             # Probe variant: linear artificial topography
โ”‚   โ”œโ”€โ”€ reproduce_mlp.py             # Probe variant: multi-layer perceptron
โ”‚   โ”œโ”€โ”€ reproduce_multiHead.py       # Probe variant: multi-headed attention
โ”‚   โ”œโ”€โ”€ reproduce_multiLayer.py      # Probe variant: multi-layer ensemble
โ”‚   โ”œโ”€โ”€ comparison_plot.py           # Bar-chart helper
โ”‚   โ””โ”€โ”€ colours.py                   # Plot palette
โ”œโ”€โ”€ analysis/
โ”‚   โ””โ”€โ”€ stego_correlations.py        # Bet/count correlations (Tables 3, 10)
โ””โ”€โ”€ scripts/
    โ””โ”€โ”€ run_model.sh                 # End-to-end pipeline

Generated data schema

Each scenario produces three files in data/raw/<model_short>/<tier>/<run_dir>/:

  • run_config.json โ€” scenario configuration (agents, coalition, mode, roles, seed, tier-specific fields).
  • agent_prompts.json โ€” per-turn system + user prompts.
  • results.json โ€” per-turn agent responses.

After extraction, data/activations/<model_short>/<tier>/ contains:

  • metadata_gen.json โ€” per-sample metadata, one entry per (agent, turn).
  • activations_gen.npz โ€” keyed layer_<idx>; rows align with metadata_gen.json.

Common metadata fields across all tiers: run_id, mode (collusion/control), role (colluder/honest), channel (public/private/thinking), phase, round, agent_name, n_tokens. Tier-specific fields (e.g. agent_vote for Core, family for Transfer, bp_corr for Stego) are also written.

Configuration

All paths and helpers are centralised in config.py:

  • DEFAULT_MODEL โ€” model used when --model is not passed.
  • MODEL_LAYERS โ€” recommended probe-layer range per model.
  • VLLM_PORT โ€” read from env var, defaults to 8020.
  • model_short_name(hf_id) โ€” derives a directory name from a HF model ID.
  • parse_layer_range("26-30") / default_layers(hf_id) โ€” layer-range helpers.

Citation

If you use NARCBench, please cite the paper.

License

See LICENSE.