Skip to content

generate: residual control-vector projection from GGUFs - #15

Closed
j-luwierski wants to merge 3 commits into
Niko1221:mainfrom
j-luwierski:control-vector-pr
Closed

j-luwierski wants to merge 3 commits into
Niko1221:mainfrom
j-luwierski:control-vector-pr

Conversation

@j-luwierski

Copy link
Copy Markdown
Contributor

What

Adds residual control-vector projection to the engine: per-layer control vectors loaded from GGUF files are projected onto the residual stream during generation, so model behaviour can be steered at inference time without modifying the weights.

New options

  • --control-vector-scaled - enable the projection with a given strength
  • --control-vector-layer-range - apply the vector only to a range of layers
  • --cvec-mode project - projection mode
  • --cvec-dir - directory with the per-layer GGUFs

Ready-to-use control vector

A refusal-projection control vector for Qwen3.8-Flash-Next can be downloaded from:

https://huggingface.co/Cudecnik/Qwen3.8-Flash-Next-refusal-projection

--control-vector-scaled FILE[:SCALE] (repeatable) loads a control-vector
GGUF - the abliterate format: direction.<L> tensors, one n_embd-wide
F32/F16/BF16 vector per layer - normalizes each direction and projects
the scaled component along it out of every hyper-connection residual
stream after that layer's FFN write.  --cvec-mode project and
--cvec-dir per-layer name the operation and its layout (the only
supported values); --control-vector-layer-range FIRST LAST restricts
which layers load directions, llama.cpp's flag, inclusive on both ends:
directions outside are never parsed nor uploaded, and a reversed range
is refused at argument validation.

The plan is static for the process (vectors uploaded once), so the
projections are ordinary nodes of the captured layer graphs.  The
projection runs in prefill, token decode and the verify window, always
right after the half's write it projects; fused GR is disabled for cvec
runs so the projection sees the completed layer residual.  A run
without the flags never touches the stream: an unloaded plan returns
before touching anything, so captured graphs and output are unchanged.

run-iq3_xxs-steered.sh now drives the Qwen3.8-Flash-Next refusal
projection vector (layers 4..44) through the new flags.
@Niko1221

Copy link
Copy Markdown
Owner

Give me an hour please.

The launcher was welded to strata-iq3_xxs.json, and the repository
serves several quantizations - each with its own config.  The script is
now run-steered.sh [CONFIG]: with no argument it uses the single
strata-*.json in the repository root (several found: it names them and
stops; none: it says so).  The config file itself is untouched - a temp
copy gains the four control-vector flags, as before.

The layer range moves to STEERING_FIRST / STEERING_LAST (default
4..44) beside STEERING_SCALE and STEERING_VECTOR, and the run is
refused loudly on a missing vector file or an out-of-range layer
window.

Verified in a sandbox: single-config auto-detection, the multi-config
listing (exit 2), an explicit path, the env overrides (scale 2.5,
layers 2..10 land on the GEN line), the missing-vector refusal, and
the four flags landing on the config's args in the engine's spelling.
@j-luwierski
j-luwierski marked this pull request as draft September 27, 2026 09:23
@j-luwierski
j-luwierski marked this pull request as ready for review September 27, 2026 09:32
@Niko1221

Copy link
Copy Markdown
Owner

Thanks for this, and sorry it sat so long. This landed in v0.1.9 as the experimental speed projection. It's the same idea: the same llama.cpp-style flags (--control-vector-scaled, --control-vector-layer-range 4 44, --cvec-mode project, --cvec-dir per-layer) and the same operation, h -= s (h.v) v on the residual after each steered layer, with a refusal-direction projection vector.

I implemented it separately rather than merging this branch:

  • one kernel that runs on every hyper-connection stream right after each layer's FFN write, in all three paths (per-token decode, the verify windows and the batched prompt path);
  • a per-request switch (a toggle in the web app, or "experimental_speed_projection": false in the API). Switched off, the output is token-for-token the stock model's;
  • setup asks for it and defaults to off, and the docs label it experimental and say what it does (fewer refusals).

The vector ships in data/experimental-speed-projection/.

So I'm closing this without merging, and none of the changes from this branch are applied (they'd conflict with the new code). Thanks for the idea and the pointer to the vector.

@Niko1221 Niko1221 closed this Sep 27, 2026
@j-luwierski

j-luwierski commented Sep 27, 2026 via email

Copy link
Copy Markdown
Contributor Author

MistVVK referenced this pull request in MistVVK/XeStrata Oct 10, 2026
FlareP1 pushed a commit to FlareP1/Strata that referenced this pull request Oct 13, 2026
…e: a re-split GGUF may name the architecture on every shard; a later shard must name the first one's

Upstream's ccfdc2e (in this branch since 0.1.43) already ran the guard on the first shard only; the pull request's
form adds the same-architecture check for the later shards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Niko1221 added a commit that referenced this pull request Oct 23, 2026
…never says the same version twice; max_tokens below 1 is a 400 (TUI stress X01, #18, #15)

X01: TestStartFailed has one text builder (TestStartFailed.plain): what did not work in plain words, that nothing was installed and the current engine is unchanged, and one next step (`strata update --force`, or a plain retry when the card was busy). The engine's own words stay in .reason, the state file and the JSON. The app's message matcher also accepts the new wording. Hints that a caller passes, such as the one for a busy launch, are used as they are, so the launch-holds-the-card line of f-023-tui1 composes with it.
#18: `components rollback` when an earlier rollback is still in force says the engine is already the previous one and what to run, instead of "0.1.45.3 -> 0.1.45.3".
#15 (K05): max_tokens, max_completion_tokens and max_output_tokens below 1 are a 400 naming the field (they used to mean "no cap"); an unknown model id is still answered by the loaded model, now documented in DETAILS.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants