Skip to main content

BrainPatch

A portable format and runtime for reversible activation-space interventions in frozen language models.

No fine-tuning. No weight modification. No prompt injection. No hosted service.

An open-source system from Blackdrome AI Labs.

pip install "brainpatch[transformers]"
brainpatch install ./experimental-feature-727.brainpatch
brainpatch compare --model Qwen/Qwen2.5-1.5B-Instruct \
                   --patch experimental-feature-727 \
                   --prompt "Explain why the sky is blue."
from brainpatch import BrainPatchedModel

model = BrainPatchedModel.from_pretrained(
    "Qwen/Qwen2.5-1.5B-Instruct", backend="transformers", device="auto"
)
patch = model.install("./experimental-feature-727.brainpatch")
patch.strength = 0.8
print(model.generate("Evaluate my idea."))

License Format Blackdrome AI Labs


What it is

A BrainPatch is a small file containing one or more direction vectors and the layers to add them to. Loading one installs a behavioural intervention into a model whose weights are never touched.

BrainPatch is infrastructure, not a method. It is agnostic about how a direction was discovered — difference-of-means, PCA, a linear probe, an SAE feature, or anything else — and it is not tied to any one behaviour. The project began as an SAE tool; on our own benchmark the SAE finished behind PCA and difference-of-means, and the format outlived that assumption.

LoRA / fine-tune Prompt engineering BrainPatch
changes weights yes no no
artifact size MB – GB n/a 6.7 KB (measured, below)
costs context no yes, every call no
adjustable at runtime no crudely yes, continuously
removable mid-session no yes yes
changeable during generation no no yes (token schedules)

Measured on the reference patch in this repository: 6,856 bytes, against 3,087,467,144 bytes of Qwen2.5-1.5B-Instruct weights — a ratio of 450,331×. Runtime overhead on an L4 was −1.3% (within noise) on Transformers and +2.2% (tokens/second) on vLLM, with 0.01 MB of extra VRAM.

Read this before using the example patches

The runtime is verified. The example patches' behaviour is weakly established and we label them accordingly. BrainPatch will not name a patch after a behaviour it has not demonstrated.

patch evidence what it is
experimental-feature-727 none named after its feature ID because its controls came back negative — a scale-matched random direction moved the output further from baseline than the real one. Ships as a working demonstration of the format.
experimental-commitment controlled_interventional a real, replicated effect on commitment / anti-evasion. Formerly named anti-sycophancy; that name was withdrawn because sycophantic agreement rose under it.
experimental-independent-criticism-candidate none a candidate direction, never validated.

What the research established, in order

Four experiments, four times the conclusion changed. The write-ups include the numbers that looked good before they didn't.

anti_sycophancy_v1 — negative. 198 propositions, topic-disjoint splits, true-assertion controls so "disagree with everything" cannot score as independence, criteria pre-registered before the test split was opened. The selected direction beat all ten scale-matched random directions, beat three unrelated real directions and reversed under sign inversion — but its per-item effect correlated +0.457 with response-length gap, over a threshold set in advance, and free generation moved the wrong way. Full write-up.

Three findings outlived the null:

  • The SAE came last. Both SAE variants lost to PCA, to a linear probe and to difference-of-means, and both failed the true-claim control by making the model disagree with true statements.
  • Probe accuracy is not steerability. A probe separating the classes at 100% accuracy steered worse than PCA, which uses no labels when fitting. Readable and pushable are different properties.
  • Where you inject beats almost everything else. Prompt-token steering was ~ more effective than steering generated tokens.

anti_sycophancy_v2 — negative, test split never opened. Rebuilt the dataset to remove the length confound (preferred response longer in 53% of pairs instead of 96%). The pre-registered free-generation gate stopped it at validation. It produced the most useful finding in the project, across 27 configurations that passed every log-probability gate:

corr(log-probability effect, free-generation correction gain) = −0.298

Ranking activation-steering directions by paired log-probability anti-selects for the behaviour you want in generation. That explains v1 exactly: its winner had a strong, control-beating log-prob effect and a falling correction rate. Only 27 of 330 configurations survived the true-claim guard — most directions that "work" are simply contrarian. Full write-up.

anti_sycophancy_v3 — the first positive result. Selecting by generated behaviour instead of log-probability, on a third fresh dataset, a single SAE feature injected at the prompt raised the correction rate on false user assertions from 0.233 to 0.400 on 200 held-out items — CI [+0.092, +0.242], McNemar p = 3.6 × 10⁻⁵, all 11 pre-registered gates passed. Full write-up.

anti_sycophancy_specificity_v1 — replicated the effect, then took its name away. On 407 fresh propositions with zero overlap against all prior data, the effect reproduced through the compiled artifact: correction 0.248 → 0.376, CI [+0.072, +0.184], McNemar p = 2.1 × 10⁻⁵.

Against 100 norm-matched random directions it reached the 95.0th percentile, empirical p = 0.0594 — a pre-registered pass, but 5 of 100 random directions matched or beat it. Specificity is real and weak.

Then the transition matrix showed what the headline hid. Only ~22% of the gain (+0.028, p = 0.17) was a claim moving from agreement to challenge. Most was evasive replies becoming committed ones, landing correct or sycophantic at chance (28 vs 26, p = 0.89) — and sycophantic agreement rose, 0.280 → 0.352. Removing the user's endorsement kept 75% of the effect.

So the patch was renamed experimental-commitment. It steers commitment and suppresses evasion; it does not reduce sycophancy. The evidence level was deliberately not upgraded (why). Full write-up.

artifact_fidelity_v1 — a claim of ours, refuted. We had shipped an artifact with an inverted coefficient sign that passed a cosine check at 1.0, and hypothesised that behavioural testing was therefore required to catch such defects. Thirteen controlled corruptions said otherwise: signed cosine, delta norm and execution tracing together caught all eleven real defects, and behavioural reproduction caught nothing they missed while missing two they caught. The lesson was narrower than the hypothesis — the reference had been unsigned — and it is now the brainpatch verify workflow. Full write-up.

Total compute for all five experiments: ~$4.65. None of these failures needed scale to find; they needed the next question to be asked.


Install

pip install brainpatch                    # core: format, registry, CLI. No ML stack.
pip install "brainpatch[transformers]"    # PyTorch backend (CUDA / CPU / MPS)
pip install "brainpatch[llamacpp]"        # GGUF control-vector export
pip install "brainpatch[vllm]"            # high-throughput serving
pip install "brainpatch[server]"          # OpenAI-compatible HTTP API
pip install "brainpatch[ui]"              # local web UI
pip install "brainpatch[research]"        # patch authoring: SAEs, extraction

The core wheel pulls in only typer and rich — no torch, no numpy, not even safetensors (the container is parsed by a pure-Python reader). Installing, inspecting and validating a patch works on a bare Python 3.10+.

brainpatch doctor      # which engines are installed and usable
brainpatch backends    # full capability matrix

Supported backends

Backend Status Static Schedules Server Verified against
Transformers verified Qwen2.5-1.5B bf16, NVIDIA L4
llama.cpp verified upstream b10344, Q4_K_M GGUF
vLLM verified vLLM 0.11.0, L4, OpenAI server
MLX-LM experimental never run on Apple Silicon

"Verified" means an automated acceptance suite ran against a real model and passed. "Implemented" means the adapter is written and reviewed but no hardware has confirmed it. We do not use the word "supported" for the latter.

What "verified" cost, concretely — each backend ran an automated acceptance suite against a real model:

  • Transformers: weights provably unchanged, strength=0 byte-identical to baseline, measured delta norm 28.5177 == expected 28.5177, schedules fire at the keyframe, disable/remove restore baseline.
  • llama.cpp: 0-based BrainPatch layer 18 maps to the direction.19 tensor (1-based), scale-0 output character-identical to baseline, non-zero scale changes output, no crash — on a real 1.12 GB Q4_K_M GGUF.
  • vLLM: hooks confirmed inside the worker process (Qwen2ForCausalLM, 28 layers, active_hooks: 1, cuda:0), OpenAI server serves concurrent requests with no state leak, mismatched per-request strength rejected 400.

Capability gaps are real, not oversights:

  • llama.cpp has no token schedules — a control vector is bound for a whole run and the CLI exposes no per-decode-step control.
  • vLLM has no per-request strength — continuous batching means one forward pass serves many sequences, so a per-request coefficient would change other users' output. Patch state is frozen while serving, which is what makes concurrency safe.
  • Quantization is not assumed to transfer. A direction fitted on bf16 was checked to still change output at Q4_K_M; whether it produces the same behavioural effect at 4-bit is untested.

CLI

brainpatch install <file.brainpatch | owner/repo>
brainpatch list
brainpatch inspect <name>
brainpatch validate <name> --model Qwen/Qwen2.5-1.5B-Instruct
brainpatch verify <name> --reference known-good.brainpatch --model <model>
brainpatch run "prompt" --model <model> --patch <name>
brainpatch compare --model <model> --patch <name> --prompt "..."
brainpatch chat --model <model> --patch <name>
brainpatch serve --model <model> --patch <name> --port 8000
brainpatch ui
brainpatch compile research.json --sae ./sae.pt -o out.brainpatch
brainpatch compile out.brainpatch --backend llama.cpp -o cv.gguf
brainpatch benchmark --model <model> --patch <name>
brainpatch doctor
brainpatch backends

Verifying a patch

validate asks is this file well-formed and loadable. verify asks does it do what it says. Those are different questions:

brainpatch verify ./my-patch.brainpatch     --reference ./known-good.brainpatch     --model Qwen/Qwen2.5-1.5B-Instruct
numerical
  signed_direction  FAIL  signed cosine -1.00000000 (threshold 0.999000);
                          unsigned |cos| 1.00000000 would have passed

Four levels, each catching what the one above it cannot. A level whose inputs are missing is reported skipped, never dropped — an absent check reads as a pass.

level needs catches
structural nothing corrupt archive, malformed manifest, wrong shape, wrong base model
numerical --reference wrong direction including sign, wrong magnitude
execution --model wrong layer, wrong injection site, lost schedule
behavioural a recorded result drift that survives all of the above

We built this because we shipped a sign-inverted artifact that passed every file-level check. Measured across thirteen controlled corruptions: schema, checksum, shape and model-compatibility caught 0 of 11 real defects; layer and site defects were invisible to every file-level check including signed cosine. Only tracing the runtime found them.

Checksums prove a file was not modified. They prove nothing about whether it encodes the right intervention. Full detail: docs/verification.md.

OpenAI-compatible server

brainpatch serve --model Qwen/Qwen2.5-1.5B-Instruct --patch my-patch --port 8000
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
client.chat.completions.create(model="qwen", messages=[{"role": "user", "content": "hi"}])

Existing clients work unchanged. Patch strength is configured at startup, not per request — with a shared model, honouring a per-request strength would alter other in-flight requests' output. A mismatched brainpatch extra field returns a clear 400 rather than being silently ignored.

The .brainpatch format

A ZIP containing only inert data:

manifest.json         what to add, where, how strongly
vectors.safetensors   the direction vectors
checksums.json        sha256 of every member
README.md             optional

A patch cannot execute code. No pickle, no scripts. The loader reads members by exact name, rejects unexpected members, absolute paths, .. traversal, symlinks and zip bombs, and verifies every checksum before use. Archives are byte-deterministic, so a published patch has a stable hash.

Compatibility is enforced in three modes — strict (default; model id and revision must match), architecture, and unsafe — because a direction fitted in one model's basis means nothing in another's.

Full specification: docs/patch-format.md.

Architecture

flowchart TD
    REPO["patch repository<br/>(Hugging Face, a file, anywhere)"]
    ART["tiny .brainpatch<br/>~6 KB"]
    REPO --> ART

    ART --> LOCAL["local workstation"]
    ART --> CLOUD["cloud server"]

    LOCAL --> TF["Transformers"]
    LOCAL --> LC["llama.cpp"]
    LOCAL --> MLX["MLX"]
    CLOUD --> VLLM["vLLM"]

    TF --> FROZEN["frozen base LLM<br/>weights untouched"]
    LC --> FROZEN
    MLX --> FROZEN
    VLLM --> FROZEN

    style ART fill:#fff4e6,stroke:#d99b4a
    style FROZEN fill:#eefaf0,stroke:#4ad97a

The runtime knows nothing about where a patch was trained. Separately, the research toolkit is how patches are made:

flowchart TD
    A["activation extraction"] --> B["sparse autoencoder"]
    B --> C["feature discovery"]
    C --> D["causal validation<br/>+ controls"]
    D --> E["patch compile"]
    E --> F["publish"]
    F -.-> G["a .brainpatch file"]

    H["Modal · local GPU · cluster<br/>(optional research backend)"] -.-> A

    style G fill:#fff4e6,stroke:#d99b4a
    style H fill:#eef2ff,stroke:#7a8ad9

Offline

Once the model, patch and backend are local, BrainPatch needs no network. No telemetry, no phone-home, no hosted dependency. --offline refuses network access outright.

How it works

A forward hook on decoder block L adds strength × coefficient × vector to the residual stream. That is the entire mechanism.

Two guarantees the test suite enforces on real hardware:

  • strength = 0 is byte-identical to baseline. Not approximately — when the resolved edit list is empty the tensor is never touched, so there is no arithmetic to round. Verified: 0 applied passes, identical output.
  • Weights are never modified. Verified by comparing layer-18 weights before and after a patched generation.

Evidence levels

Every patch declares one, and the CLI prints it everywhere:

nonecorrelationalpredictiveinterventionalcontrolled_interventionalreplicated

The top rung is deliberately not called "causal": passing scale-matched controls once, on one model at one layer with one prompt set, is evidence consistent with a causal effect, not a demonstration of causation.

Experimental evidence

, and what its controls actually showed:

condition divergence from baseline delta norm
zero 0.000 (6/6 byte-identical) 0.0
positive 0.710 28.5178
random direction 0.847 28.5178

A scale-matched random direction moved the output further than the real feature. There is no evidence of a feature-specific effect. A later audit found the cause: the max_activation selection rule picked a degenerate cluster of 32 near-duplicate features that all fire on the same rare token.

Full account, including a retracted control: RESEARCH_LOG.md.

Create your own patch

pip install "brainpatch[research]"
brainpatch compile my-research-patch.json --sae ./sae_latest.pt -o my.brainpatch

The compiler materialises SAE decoder columns into raw residual-space vectors, so the artifact is self-contained. Verified numerically: the compiled patch produced a delta norm of 28.5177 against the research pipeline's 28.5178.

Vectors from any method work — difference of means, PCA, a learned controller. The runtime does not care; provenance is recorded in metadata.

Research toolkit

brainpatch/research/ holds activation extraction, Top-K SAE training, feature discovery, causal validation and patch search. Installed only by the research extra and never imported by the runtime.

Reproducing our experiments with Modal

This repository's experiments ran on Modal because the development machine deliberately carries no ML stack. Modal is how we build BrainPatch; it is not how you use it.

pip install "brainpatch[modal,research]"
modal run modal_app/app.py::smoke_pipeline
modal run modal_app/app.py::test_transformers_backend
modal run modal_app/app.py::sae_unit_tests

Total metered spend for the entire project to date: $2.08, including all three backend verifications and the behavioural experiment.

See docs/modal-infrastructure.md.

Testing

pytest                                          # 601 pure-Python tests, no ML stack
modal run modal_app/app.py::sae_unit_tests      # SAE maths (needs torch)
modal run modal_app/app.py::test_transformers_backend   # real-model acceptance

Limitations

  1. One behavioural patch has replicated; none is strongly specific. experimental-commitment reproduced its effect on fresh data through the compiled artifact, but sits at only the 95.0th percentile of 100 matched random directions (empirical p = 0.0594). experimental-feature-727's controls are outright negative.
  2. Behavioural claims are single-model. Everything behavioural was measured on Qwen2.5-1.5B-Instruct at one layer and one magnitude. MLX has never run on Apple Silicon.
  3. experimental-commitment is Transformers-only. Its prompt-only injection site is inexpressible in llama.cpp and vLLM — a capability boundary, not a defect in either. The backends themselves are verified for static patches.
  4. The vLLM adapter uses vLLM internals. vLLM exposes no public activation-hook API, so that path is version-sensitive.
  5. Quantization is untested. A direction fitted on bf16 is not guaranteed to behave the same at Q4.
  6. SAE features may be polysemantic, and steering can affect unrelated capabilities — the smoke test saw 9/10 → 8/10 on ten probes, far too small a sample to establish degradation.
  7. Results depend on model revision and generation settings. Both are pinned.

Repository layout

brainpatch/
├── patch/        format, loader, registry, compiler, validation  (no ML stack)
├── runtime/      backend contract, capabilities, scheduling, model API
├── backends/     transformers · llamacpp · vllm · mlx
├── server/       OpenAI-compatible API
├── ui/           local Gradio app
├── schemas/      v0.1 research patch, SAE config, manifests
├── verify/       artifact-fidelity checks and the corruption suite  (no ML stack)
└── research/     SAE training, extraction, discovery, validation
modal_app/        research + integration-test orchestration (optional)
experiments/      frozen research record, including the negative results
patchbench/       frozen evaluation scaffolding, not part of the wheel
tests/            pure-Python tests · tests/remote/ needs torch

Links

Base model Qwen/Qwen2.5-1.5B-Instruct (Apache-2.0), not redistributed. Corpus Salesforce/wikitext (CC BY-SA 3.0), not redistributed.

Apache-2.0. Maintained by Blackdrome AI Labs.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

brainpatch-1.2.0.tar.gz (393.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

brainpatch-1.2.0-py3-none-any.whl (383.7 kB view details)

Uploaded Python 3

File details

Details for the file brainpatch-1.2.0.tar.gz.

File metadata

  • Download URL: brainpatch-1.2.0.tar.gz
  • Upload date:
  • Size: 393.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for brainpatch-1.2.0.tar.gz
Algorithm Hash digest
SHA256 f135a44497f8ae32ffaaca9c11556e1da2fb084df30a26801da871f9a8628b64
MD5 2196b895c4a898022081bf72102b0f5b
BLAKE2b-256 829d67e2a863746fdb12b72a767f952d53cbc9652a8433a45e86c07c68b9d98d

See more details on using hashes here.

File details

Details for the file brainpatch-1.2.0-py3-none-any.whl.

File metadata

  • Download URL: brainpatch-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 383.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for brainpatch-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 56317e13853bfecbd8798765d9878e2dd5175158ee7c711d60700abe7dbb6240
MD5 fc991eb9b2ab3a144db9c07def423d67
BLAKE2b-256 837dd22696ba092a40d994d695c11f8925f2595bdb3c0318a40172cf7d63f5c3

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page