Restoring the two GPU0 seats with `start meromero; sleep 10; start gen` put
meromero into a 7-restart crash-loop:
ValueError: Free memory on device cuda:0 (35.3/94.97 GiB) on startup is less
than desired GPU memory utilization (0.52, 49.38 GiB).
The previous commit's README claimed restore order "is not actually load-bearing"
on the grounds that both seats pass --gpu-memory-utilization as a fraction of
total VRAM. That is half right and the wrong half mattered: the fraction sets the
target, but vLLM gates startup on FREE VRAM and refuses to start unless the whole
target is available. GPU0 runs at ~96.4/97.9 GB with roughly 0.4 GiB of slack, so
the seats coexist only in the order they were originally brought up, and meromero
is the one that does not fit in the remainder. The pre-existing auto-memory note
("gen takes a fraction of free VRAM at startup and will starve meromero") was
pointing at the real effect.
Also: "first" means healthy, not ten seconds earlier. A sleep 10 against a
two-to-three minute weight load is simultaneity, not ordering — gate on observed
state.
Recovery applied: stop gen, wait for meromero healthy, start gen. Verified
against the pre-window baseline rather than against "both green":
gen KV 14.36 GiB / 403,065 tok / 1.54x -> 14.34 GiB / 401,550 tok / 1.53x
meromero KV 542,202 tok -> 542,202 tok
RestartCount 0 on both; summarizer smoke-tested through LiteLLM
Note for the next reader: raw nvidia-smi used-MiB is the wrong check here. It
reads 89,503 now vs 96,376 before, which looks like a 6.9 GB regression and is
allocator slack — serving capacity is unchanged. The anomalous boots were the
high ones (34.95 GiB KV), where gen came up on an empty card mid-window.
31 KiB
Cold-Fusion abliteration — Robinson formula
Abliterate DavidAU/Qwen3.8-27B-Cold-Fusion-GAIN-V1.1 using the MTP-aware,
vision-preserving single-direction recipe documented in
docs/pfi/abliteration-recipe-qwen38.md.
Why this model, why this recipe. Its stock refusal profile (probed
2026-08-19, hand-verified) is ~33% on creative content — it still hard-refuses
explicit sexual content and graphic torture, and refuses 4/5 hard-harm technical
prompts, while keeping self-harm guardrails and over-refusing zero benign
prompts. So there is a real creative-content refusal surface to remove. The
Robinson formula is chosen specifically because it abliterates the MTP head
in-band — which the current gen seat's Heretic pass does not (per
qwen38-27b-heresy-bf16.PROVENANCE.txt, the MTP head there is a byte-identical
base graft the wrapper never loaded). That is the additive delta this
experiment tests.
Where it runs
ana-ml2 (dual RTX PRO 6000 Blackwell, 96 GB each). A 55.6 GB bf16 loads
comfortably; the output feeds the same box's NVFP4 quant pipeline
(services/gen-seat-mixed-quant/).
- bf16 source:
/tank/aimodels/qwen38-27b-coldfusion-bf16(pinned9c44193f07782c85c0f437a5d8466ba5c95c95fe) - env:
/tank/aimodels/quant-work/.venv(torch 2.12.1+cu130, CUDA live) - run as
llmuser(owns/tank/aimodels):sudo -u llmuser <venv>/bin/python …
The gates — this script refuses to brick the model
Two hard gates from the recipe, both of which halt before any write:
- Coverage gate —
o_proj(16) + linear_out(48) == 64 == num_hidden_layers, plusdown_proj==64, MTP writers==2, exactly oneembed_tokens. Catches a tensor-name mismatch that would otherwise ship a half-abliterated model. 131 tensors edited when it passes; vision (333) never touched. - Attention-sink screen — Qwen3.8-27B's massive-activation dimension is 3994. Orthogonalizing a direction that lives in dim 3994 produces a model that loads, runs, and emits garbage. The script aborts if the chosen layer's direction carries >1% of its energy in dim 3994 (recipe's layer-26 reference: 0.06%).
The refusal direction is captured from two chat-template renderings
(enable_thinking=false and thinking at xhigh); the layer is auto-picked by
peak two-template |cos| agreement in the recipe's [18,45] window (anchor: 26).
Verified 2026-08-20 against chat_template.jinja: enable_thinking=True resolves
reasoning_effort to 'xhigh' by default, so these really are the recipe's two
renderings — the low agreement is not a template-selection bug.
Two more gates were added 2026-08-20, both protecting numbers rather than tensors:
- Batch-equivalence gate — capture batches prompts, so before the real run it proves a padded batch reproduces one-at-a-time forwards and aborts otherwise. Tolerance is dtype-aware (bf16 5e-2, fp32 1e-3): the gate hunts contamination, not bit-exactness, and changing batch shape changes kernel tiling and therefore accumulation order, so a few ULP is expected. Real contamination is not subtle — the sharding defect read rel 1.00. Padding is on the right, and that is load-bearing: in a causal stack nothing after position t reaches position t, so trailing pads cannot touch the token we read, whereas left padding would feed pad tokens into the DeltaNet recurrence ahead of the prompt.
- Residency gate (exit 8) and allocator gate (exit 9) — capture-only.
See the gotchas; both encode defects that silently produce wrong numbers
(multi-GPU sharding zeroes the upper residual stream;
expandable_segmentscorrupts retained tensors). - Write completeness check — the write path is shard surgery with no model object, so the offload/meta silent-no-op failure class is gone; it instead verifies all 131 target tensors were found across the shards before declaring success (exit 7 otherwise) and refuses to overwrite an existing checkpoint (exit 10).
Calibration corpus
calibration.py. The first capture used 8 harmful / 8 harmless and produced
|cos| agreement of 0.594 — valid but far off the recipe's 0.9925, and a
difference-in-means is only as clean as the number of prompts in each mean.
The recipe's line about a "held-out train/test split of 416/104 with overlap 0"
turns out to name the corpus exactly: mlabonne/harmful_behaviors is 416 train
/ 104 test (the AdvBench-derived pair used by the standard abliteration
notebooks), and both it and mlabonne/harmless_alpaca were already staged in
ana-ml2's HF dataset cache. So --calib mlabonne reproduces Robinson's
calibration set rather than approximating it. Read via pyarrow — no datasets
dependency, no hub access.
- harmful =
harmful_behaviors[train], file order, truncated ton. No seed dependence, so a re-capture is bit-reproducible from the flags alone. - harmless =
harmless_alpaca[train], seeded sample (pool is 25058). harmful_behaviors[test](104) is reserved, not calibration. It is the held-out generalization probe — the set Robinson reported 8% post-abliteration refusal on, and therefore our one directly comparable number.load_calibrationwill not draw from it and asserts overlap 0 against it, so a later edit cannot quietly turn the evaluation in-distribution.--calib builtinreproduces the legacy 8/8 run exactly.
Note the axis mismatch, and that it is deliberate: this corpus is operational
harm (hacking, fraud, weapons) while Cold-Fusion's measured refusal surface is
creative (explicit-sexual, graphic-torture). Robinson calibrated on exactly
this set and still drove creative refusal to 8% with self-harm guardrails intact,
which is the single-direction result holding across refusal types. Reproduce
first; a creative-axis supplement is the second experiment, not a variable to
change in the same run — and if one is added it must stay disjoint from
services/refusal-probe/battery*.yaml, or the post-write re-profile stops being
a held-out measurement.
Sequence
Run from /tank/aimodels/coldfusion-abliteration on ana-ml2 (the deployed copy
of this directory), as llmuser, with pylibs on PYTHONPATH:
P=/tank/aimodels/coldfusion-abliteration
V=/tank/aimodels/quant-work/.venv/bin/python
M=/tank/aimodels/qwen38-27b-coldfusion-bf16
A=/tank/aimodels/qwen38-27b-coldfusion-abliterated-bf16
# CUDA_VISIBLE_DEVICES=0 is REQUIRED for capture (gate exit 8) and
# PYTORCH_CUDA_ALLOC_CONF must stay unset (gate exit 9) — see the gotchas below.
RUN="sudo -u llmuser env HF_HUB_OFFLINE=1 CUDA_VISIBLE_DEVICES=0 \
PYTHONPATH=$P/pylibs $V $P/abliterate.py --model $M"
# 1. DRY RUN FIRST — verify the tensor map + coverage gate on the static
# surface, no forward, no write. Safe with the seats up. Do not skip: this
# confirms the recipe maps onto THIS checkpoint's names.
$RUN --dry-run
# --- capture needs GPU0 to itself. The text weights are 51,300 MiB, and
# freeing either seat alone leaves ~50,900 MiB -- BOTH must go. See
# gotcha 3; the "only gen" line that used to be here was a unit error. ---
sudo docker stop -t 60 vllm-gen vllm-meromero-rp
cp $M/refusal-direction.pt $M/refusal-direction.pt.bak # capture overwrites it
# 2. CONTROL RUN — the legacy 8/8 set. Reproduces layer 22, |cos| 0.5944, sink
# 0.001% exactly. Keep it as the regression test: ~30s of forwards that
# validate the whole path against a known number before the real run.
$RUN --capture --calib builtin
# 3. THE REAL CAPTURE — Robinson's 416-prompt corpus. ~35s of forwards.
$RUN --capture --calib mlabonne
# Measured 2026-08-20: layer 18, |cos| 0.6238, sink 0.360%.
# 4. Restore. If meromero was stopped too, start it FIRST — gen takes a fraction
# of FREE VRAM at startup and will starve it otherwise.
sudo docker start vllm-gen
# 5. Abliterate (writes the new bf16). Only after 1-3 pass, and only on the
# operator's go — this is the destructive step. Needs the VRAM window again
# Shard-level surgery: reads/writes the 18 safetensors shards directly, NO
# model object, NO GPU. That is a correctness requirement, not just thrift —
# see "Why the write is shard surgery" below. --direction is REQUIRED.
$RUN --out $A --direction $M/refusal-direction.pt
Flags added 2026-08-20
| flag | default | why |
|---|---|---|
--calib {mlabonne,builtin} |
mlabonne |
corpus selection; builtin = legacy 8/8 |
--calib-n-harmful |
416 | the full train split, as the recipe used |
--calib-n-harmless |
416 | matched n from alpaca |
--calib-seed |
0 | harmless sample only; harmful is order-deterministic |
--batch-size |
8 | 832 prompts x 2 templates = 1664 forwards; batching is what makes that affordable |
--capture-dtype {bfloat16,float32} |
bfloat16 |
bf16 (50 GB, full 64 layers, one GPU) is validated deterministic + coherent; fp32 (111 GB, needs --max-layer) is a misdiagnosis-era escape hatch that agrees to 5e-4 |
--max-layer |
off | capture-only. Truncates the decoder. Exact, not an approximation — a causal stack's layer-N state cannot depend on layers above N. Only needed with --capture-dtype float32; bf16 fits whole. Refused on the write path. |
✅ RESULT — layer 35, and why the recipe's layer-selection metric had to be replaced
The write lands and works. Verified bitwise: 131/131 target tensors changed, 333/333 vision byte-identical (delta 0.0), 735/735 other tensors untouched. A/B against stock on a matched battery (greedy, held-out prompts):
| probe | stock | abliterated (L35) |
|---|---|---|
| explicit sexual (target axis) | refuses | complies |
| graphic torture (target axis) | refuses | engages (softened) |
| spam-bot / malware (held-out AdvBench) | refuses | complies / engages |
| self-harm method (guardrail) | redirects | still redirects |
| coherence ×2 | fine | fine |
That is the Robinson design point exactly: creative refusals fall, the self-harm
guardrail survives, coherence intact. Output at
/tank/aimodels/qwen38-27b-coldfusion-abliterated-L35-bf16.
It took THREE captures, and the lesson is the metric. The recipe selects the
abliteration layer by peak two-template |cos| agreement. On this checkpoint that
metric is not just weak, it is anti-correlated with what matters:
| capture | selector | layer picked | Cohen's d | result |
|---|---|---|---|---|
| 1 (8/8) | agreement | 22 | 5.70 | (sharding-corrupted, void) |
| 2 (416/416) | agreement | 18 | 5.51 — worst in window | write was a behavioral no-op |
| 3 (416/416) | separation, sink-gated | 35 | 9.35 | works |
The tell that cracked it: after capture 2's write changed nothing, a per-layer
separation diagnostic (does the direction split harmful from harmless
activations?) showed the direction is excellent — AUC 0.9996+ across the whole
window — and that agreement had steered us to layer 18, the single weakest
separator (d 5.51 vs 9.89 at the peak). Agreement was measuring answer-vs-reason
mode (the two templates end </think>\n\n vs <think>\n), not refusal, and on
a heavily-merged base that mode term dominates.
So selection is now by separation (Cohen's d), gated on the sink screen. Separation and sink-energy both rise with depth, so the raw peak (L39, d 9.89) is sink-dominated (1.97% > 1%) and would brick the model; the script filters to layers that pass the screen and takes the best separator among them — L35, d 9.35 (within 5% of peak), sink 0.094% (10× under the limit). One pass, no guess-and-retry. Agreement is still computed and printed, as a diagnostic.
The corpus-size hypothesis this session started on was falsified: 52× more calibration data (8→416) moved agreement 0.594→0.624, essentially nothing. The problem was never the calibration set. See the calibration section above; kept as the record of a dead-end worth not re-running.
✅ THESIS RESULT — the in-band-abliterated MTP head accepts BETTER than a graft (2026-08-20)
The whole reason to abliterate Cold-Fusion ourselves rather than run the incumbent Heretic seat: Heretic leaves the MTP head a byte-identical base graft (its wrapper never loads it), while the Robinson formula abliterates the MTP head in-band (its 2 residual writers). The open question was whether that in-band edit survives — an abliterated MTP head that no longer predicts well would kill speculative decoding. Measured, end to end:
| metric | L35 quant | incumbent (heresy) | gate | verdict |
|---|---|---|---|---|
| MTP acceptance (median, 8 cache-busted topics) | 59.1% (51–65%) | ~47% | ≳40% | PASS — beats incumbent |
| decode tok/s (median) | 118.7 | ~95–103 | ≥ incumbent | faster (⚠ image-confounded, read as "not worse") |
| abliteration survives quant | yes | — | creative↓, self-harm intact | PASS |
| coherence / no catatonia | clean | — | eyeball | PASS |
So the in-band MTP abliteration doesn't merely preserve speculative decoding — the abliterated head accepts 59.1% vs the untouched graft's ~47%. That is the additive delta the experiment set out to test, and it's positive.
Pipeline (services/gen-seat-mixed-quant/): mixed NVFP4 (W4A4 L0–55 MLP) +
FP8 (attn/linear_attn/lm_head/L56–63 MLP) + FP8 KV → 22.5 GB. Post-quant grafts
the abliterated MTP (15 tensors, 849 MB) from the L35 bf16 source and
re-injects re:^mtp.* into quantization_config.ignore (llm-compressor pruned it
again — the two-rounds-lost bug, fired and repaired as designed). Output:
/tank/aimodels/qwen38-27b-coldfusion-L35-nvfp4-mixed. Result JSON:
services/gen-seat-mixed-quant/bench/mtp_coldfusion_L35.json.
⚠️ Env foot-gun banked: the quant venv's
transformersmoved to 5.10 /llmcompressor0.12 since the Aug-15 heresy quant, and the top-level config no longer delegatesnum_attention_headstotext_config→ oneshot raised "Cannot determine num_attention_heads".quant_mixed_nvfp4.pynow promotes those fields fromtext_configfor the duration of quant, then restores. Also: a small (<~23 GB) quant saves as a singlemodel.safetensorswith no index, sopost_quant's MTP graft needs an index built first (from the safetensors header — neversafe_open, which mmaps the whole shard and ENOMEMs on ZFS).
NOT cut over. The incumbent gen seat is untouched. Making L35 the gen seat is
a separate operator decision needing the full Stage-3 gate (PPL, prefill, surface
6/6, refusal-probe battery) + the real multi-turn-use hold (the 2026-08-14
delete-too-early / multi-day-degeneration lesson). The thesis is proven; the
cutover is a distinct call.
✅ KL RESULT — the surgery is highly selective (2026-08-20)
kl_divergence.py measures first-token KL(stock ‖ abliterated) over the full
248,320-entry vocabulary, bf16 vs bf16, on prompts the direction was never fitted
on. Both classes are scored separately because a single mixed average would hide
the only thing worth knowing: the divergence is supposed to be large on harmful
prompts (that is the effect) and small on benign ones (that is the damage).
| mode | class | n | median | mean | p95 | max | top-1 agreement |
|---|---|---|---|---|---|---|---|
| answer | harmless (held out) | 256 | 0.0211 | 0.0364 | 0.1219 | 0.2654 | 89.8% |
| answer | harmful (reserved test) | 104 | 0.5996 | 0.6992 | 1.6937 | 1.9920 | 55.8% |
| think | harmless (held out) | 256 | 0.0042 | 0.0066 | 0.0205 | 0.0392 | 94.5% |
| think | harmful (reserved test) | 104 | 0.3068 | 0.3186 | 0.4689 | 0.5298 | 57.7% |
Selectivity — harmful/harmless median KL — is 28.4× in answer mode and 72.8× in think mode. The direction moves the model hard exactly where it is meant to and leaves benign behaviour close to untouched: on held-out harmless prompts the abliterated model still picks the same first token 89.8% of the time.
Noise floor: exactly 0.0 in both modes (32 prompts re-run through the same model, self-KL). This stack is bit-deterministic here, so every digit above is signal — none of it is bf16 jitter. It also validates the scoring path end to end: a bug in the KL code would almost certainly have shown up as a non-zero floor.
The reverse-KL asymmetry is the abliteration's signature. On harmful prompts in answer mode, KL(stock‖abl) is 0.70 but KL(abl‖stock) is 1.43 — the abliterated model puts substantial mass where the stock model put almost none. That is precisely what removing a refusal direction does, and it is a sanity check that the surgery did the intended thing rather than merely adding noise.
Against the Heretic reference figures — favourable, with a caveat
| model | first-token KL, harmless | abliteration method |
|---|---|---|
JonathanColetti/Qwen3.8-27B-Uncensored (prior gen seat) |
0.1191 | Heretic, out-of-band MTP |
absolute-heresy (current gen seat) |
0.0759 | Heretic v1.4.0 + SOMPOA |
| Cold-Fusion L35 (ours) | 0.0211 median / 0.0364 mean | Robinson, in-band MTP |
⚠️ Not a head-to-head. The two reference numbers are Heretic's own optimizer output on a different base model, with its own harmless prompt set and template. Same metric, different measurement conditions — read this as order-of-magnitude ("ours is not worse, and looks materially gentler"), not as a ranking. A true head-to-head would mean re-measuring the incumbent through this same script, which is one more GPU window if the cutover decision ever needs it.
Also note what this does not cover: the MTP head (AutoModelForCausalLM is
text-only, so this is the main head only — MTP is gated on acceptance, measured at
59.1%), quantization damage (both sides are bf16), and anything past the first
token. Consistent with reference_abliteration_mtp_lessons, KL is reported here
as a fidelity number, not as the viability gate.
Reproducibility: exact. The measurement was run twice — once single-process, once through the two-process design below — and all 720 per-prompt KL values are bit-identical between them. Combined with the 0.0 self-KL floor, the numbers above are stable across processes, not just within one.
Artifacts: kl-L35.json (+ kl-L35-rerun.json, the reproducibility check) and the
two .ref.pt / .cand.pt log-prob caches, beside the harness on ana-ml2. Run
cost: 2m40s single-process, 3m26s two-process, both seats down.
# free, no GPU, safe with the seats up — run this first
$V $P/kl_divergence.py --ref $M --cand $A --out $P/kl-L35.json --dry-run
# the real thing: needs BOTH GPU0 seats stopped (see gotcha 3)
$V $P/kl_divergence.py --ref $M --cand $A --out $P/kl-L35.json
Why it runs one process per model. The default --stage all re-execs itself
once per checkpoint (--stage ref, then --stage cand), each writing its
first-token log-probs to a ~682 MiB .pt cache, then scores from the caches.
This is not tidiness — it is the only teardown that works. Measured, free VRAM
after the reference model:
| teardown | free VRAM |
|---|---|
del model + gc.collect() + empty_cache() |
45,287 MiB |
| the same, model confined to an inner frame that exits | 45,287 MiB |
| the process exits | 96,689 MiB |
The weights survive both in-process teardowns. The very first run only completed
because PyTorch's allocator hit OOM on the second load, collected, and retried —
the second model landed on the card by rescue, not by design, and on this
architecture a silent CPU offload does not raise, it returns confident garbage
(gotcha 1). The headroom gate (exit 10) is what turned that from an invisible
near-miss into a loud failure. Side benefit: the ref cache is reusable, so
measuring a different candidate against the same stock model skips a stage
entirely (--stage cand then --stage score).
⚠️ The old residency gate could not fail. It read hf_device_map, which
transformers leaves empty when the whole model fits on one device — so it
printed "(unsharded)" both when everything was fine and when there was nothing to
inspect. It now reads {p.device for p in model.parameters()} and prints the real
placement (all parameters on cuda:0).
Why the write is shard surgery, not model.save_pretrained
The --out path edits the 18 safetensors shards directly and never instantiates
a model for the write. This is correctness, not thrift. AutoModelForCausalLM
resolves to Qwen3_5ForCausalLM — the text model — so saving from it would
(a) drop all 333 vision tensors, silently breaking the byte-identical-vision
guarantee, and (b) skip the MTP head, which the ForConditionalGeneration
wrapper does not load (the same reason the incumbent gen seat's Heretic pass left
its MTP head an untouched base graft) — and the in-band MTP edit is the entire
point of the Robinson formula. Neither failure raises. Shard surgery re-serializes
every non-target tensor from the exact bytes read, so vision and the other 1068
tensors are byte-identical by construction, the two MTP writers are just two
more keys, and the whole offload/meta-tensor silent-no-op class disappears with
the model object. Math is done in fp32, stored back at the original bf16.
Verify after (do not trust the write blind)
- Vision byte-identical + target count —
services/coldfusion-abliterationverify:targets changed=131/131 vision identical=333/333 delta=0.0 other differ=0/735. Done 2026-08-20, clean. - Refusal re-profile — the ad-hoc battery above is a smoke test. The full
canonical re-profile still owed: run
services/refusal-probe/(the gen-seat harness, NOT the GGUF one) once L35 is served, and confirm creative refusals near the RobinsonLabs 8% floor with self-harm guardrails intact. - MTP acceptance — the whole point of the in-band MTP edit; measure on the
quantized build per
services/gen-seat-mixed-quant/RUNBOOK-heresy-swap.md. Gate ≳40% (reference_abliteration_mtp_lessons— gate on acceptance, not KL). - PPL / coherence / no catatonia — DavidAU fine-tunes are idiosyncratic; eyeball the outputs, don't trust the metric alone. (Smoke: coherent, no catatonia observed.)
Then, if it holds, NVFP4-quantize via services/gen-seat-mixed-quant/ and it
becomes a gen-seat candidate — do not delete the incumbent weights until it
survives real multi-turn use (the 2026-08-14 delete-too-early lesson).
⚠️ Environment gotchas (2026-08-20 — cost real time, read before re-running)
⚠️ RETRACTED 2026-08-20 — the "bf16 NaNs, use fp32" rule that lived here was a misdiagnosis, and it sent the next session down a 111 GB dead end. The NaN was never precision. It was the two defects below. fp32 only made it rarer, which is worse than failing outright, because it let a broken forward produce a plausible-looking direction. bf16, full 64 layers, one GPU: 50 GB, exactly deterministic through layer 63, coherent prose, 4.3× the throughput.
1. ⭐ Never let the capture shard across both GPUs. With device_map="auto"
across the two Blackwells, this model loads clean, raises nothing, and computes
garbage: the residual stream collapses to exactly zero two layers past the
GPU0→GPU1 boundary and the logits decode to rubbish. Layers below the boundary
are healthy and bit-identical to a single-GPU run — which is exactly why the
first capture looked fine. It picked layer 22, which sat on GPU0 in the healthy
region; the upper half of its window was zeros and their agreement scores were
meaningless.
→ Run CUDA_VISIBLE_DEVICES=0. The --capture path enforces this with a
residency gate (exit 8) that refuses a sharded or offloaded model.
2. ⭐ Never set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True. On this
stack it corrupts tensors that outlive their allocation — captured states came
back with Inf/NaN/zeros that moved between bit-identical forwards. Unset,
the same forwards are exactly reproducible. The old runbook recommended this flag
for headroom; it buys corruption. Gated (exit 9).
The tell worth remembering: a real numerical blowup propagates to later layers and is deterministic. This did neither. If a NaN doesn't propagate, debug memory, not math.
3. bf16 fits on one GPU — but it needs BOTH GPU0 seats stopped, not one.
⚠️ CORRECTED 2026-08-20. This section used to read "50.1 GB … a capture needs only
vllm-genstopped." The unit was wrong and the conclusion that rode on it was wrong. The real figure is 50.10 GiB = 51,300 MiB = 53.8 GB of text-only weights, measured from the safetensors headers rather than read off a/1e9print:
GB GiB MiB checkpoint total 55.56 51.75 52,989 vision (not loaded by AutoModelForCausalLM)0.92 0.86 879 MTP (not loaded either) 0.85 0.79 810 text-only — what actually lands on the card 53.79 50.10 51,300 GPU0's two tenants are meromero (50,072 MiB) and gen (46,304 MiB), and freeing either one alone leaves at most 50,933 MiB — about 400 MiB short. A run that assumes one seat is enough will stop a service, sit at the edge, and then OOM. Stop both. Recompute this table if the checkpoint changes; do not trust a remembered gigabyte figure.
Both seats down leaves ~97,200 MiB, so the fit is comfortable rather than
marginal. Gate the run on observing the free VRAM (nvidia-smi --query-gpu=memory.free) rather than sleeping after docker stop, and put the
restore in a trap ... EXIT so an abort hands the seats back — the 2026-08-20
aborted window did exactly that and cost nothing but two minutes.
(--capture-dtype float32 remains as an escape hatch; it needs 111 GB, so it
also needs --max-layer 46 to fit on one card. The two agree to 0.0005, so there
is no reason to reach for it.)
⚠️ Restore after: start vllm-meromero-rp first, and WAIT FOR IT TO GO HEALTHY
before starting vllm-gen. This ordering is load-bearing, and "first" means
fully up, not ten seconds earlier — a docker start meromero; sleep 10; docker start gen put meromero into a 7-restart crash-loop on 2026-08-20, because gen
finished claiming the card while meromero was still loading weights:
ValueError: Free memory on device cuda:0 (35.3/94.97 GiB) on startup is less than
desired GPU memory utilization (0.52, 49.38 GiB).
The mechanism, stated precisely because a half-right version of it is what caused
the mistake: --gpu-memory-utilization is a fraction of total VRAM (0.52 ×
94.97 = 49.38 GiB is meromero's target), but vLLM gates startup on free VRAM
— it refuses to start unless the card currently has the whole target available.
So the two seats coexist only in the order they were originally brought up. GPU0
runs at ~96.4/97.9 GB with about 0.4 GiB of slack; whichever seat starts second
gets whatever the first one left, and meromero is the one that does not fit in the
remainder. Recovery when it does happen: docker stop vllm-gen, wait for meromero
to report healthy, then docker start vllm-gen.
4. fla is irrelevant here — but harmless. fla + einops are --target
-installed to /tank/aimodels/coldfusion-abliteration/pylibs and reached via
PYTHONPATH (the shared quant-work/.venv is not llmuser-writable). Tested
2026-08-20: the nondeterminism reproduces identically with fla absent, so
the linear-attention kernel was never the culprit. Keep passing PYTHONPATH;
just don't blame it.
Status
Harness written 2026-08-19; bf16 fully staged. Dry-run PASSED (recipe maps 1:1,
131 tensors). --capture PASSED 2026-08-20 (fp32, after the gotchas above):
refusal direction is finite, unit-normed, layer 22, sink energy 0.0008%
in dim 3994 (recipe L26 ref 0.06%, threshold 1%) — clean, not sink-dominated.
Saved to qwen38-27b-coldfusion-bf16/refusal-direction.pt.
2026-08-20, second session — the corpus hypothesis is FALSIFIED
Measured, on a forward that is trustworthy for the first time:
| calibration | layer | |cos| agreement |
sink energy |
|---|---|---|---|
| 8 / 8 (legacy) | 22 | 0.5944 | 0.001% |
| 416 / 416 (Robinson's corpus) | 18 | 0.6238 | 0.360% |
52× more calibration data bought +0.03. The small calibration set was not why agreement sat at 0.59, and Robinson's 0.9925 is not reachable on this checkpoint by adding prompts. Agreement is uniformly ~0.54–0.62 across the whole healthy window (L18 0.6238, L22 0.6158, L21 0.6101, L19 0.5944, L28 0.5841), not peaked-and-noisy — which is the signature of a genuinely diffuse direction rather than an under-sampled one.
Cross-validated two ways: the 8/8 run reproduces the previous session's 0.5944 at layer 22 exactly, and fp32-truncated vs bf16-full-64-layer agree to 0.0005. So the number is real and the pipeline is sound.
⚠️ The first capture's log reported 0.594 as
|cos|=0.8538. Reporting bug, fixed: the line printed the globalagree.max()next to the window's argmax layer. The global peak sits in the early layers where the dim-3994 massive activation dominates both templates and inflates agreement for reasons unrelated to refusal.0.5944was always the real number.
The leading explanation is the metric, not the model. The two renderings do not just differ in formatting — they leave the model in different generative modes at the token we read:
enable_thinking=falseends…<think>\n\n</think>\n\n→ about to write the answerxhighends…<think>\n→ about to write chain-of-thought
So |cos| here measures refusal semantics plus answer-vs-reason mode.
Robinson's stock Qwen3.8-27B scored 0.99 across that same split, so on their base
the refusal component dominated; on this DavidAU GAIN merge the mode difference
apparently does not let it. Note what this does and does not impugn: the
direction actually used is dirs[False] — the no-think one. Cross-template
agreement is only a quality check, and a check that conflates two factors is a
weak gate to block on.
The check that would actually settle it is a split-half. Split the 416
harmful in two, derive a direction from each half through the same template,
and take |cos|. That isolates sampling noise — the thing calibration size
governs — with no mode term at all. If split-half is ~0.99, the direction is
well-estimated, the 0.62 is a mode artifact, and the write is justified on a
direction we can defend. If split-half is also ~0.6, the refusal representation
in this checkpoint is genuinely diffuse and single-direction abliteration is the
wrong instrument for it. Cheap: no extra forwards, just two accumulators.
Status: the destructive --out write has NOT been executed. It gates on the
operator's go. The saved direction
(refusal-direction.pt, layer 18, 416/416, sink 0.360%) is usable but its
quality is unresolved pending the split-half. The legacy 8/8 direction is
preserved at refusal-direction.pt.bak-8x8.