In #206 you said that if fused-expert abliteration could be made to work without direct weight modification, you'd be happy to hear about it. Here's a mechanism; I'd like your read before writing code.
Hook the output of each layer's Qwen3_5MoeSparseMoeBlock and project the refusal direction r_hat (unit vector) out of it: out -> out - alpha * (out . r_hat) * r_hat. Because down_proj is bias-free and the map is linear, it commutes with the top-k routing weights and the scatter, so projecting the block output is exactly equivalent to orthogonalizing the down_proj of every routed expert and the shared expert (delta_W = -alpha * r_hat * r_hat^T W); sparse routing adds no approximation.
This differs from the closed #206/#207: no weight is touched during the search. The per-layer alpha is a mutable scalar the hook reads, and reset_model zeroes it back to identity exactly like it zeroes lora_B today; the Optuna kernel and direction selection are reused unchanged. It's the limitation you noted in #339 — PEFT modifies the residual stream through Module hooks, which a raw nn.Parameter lacks — worked around by hooking the parent Module instead.
The one thing I want your call on before building: do you want this in model.py now, or held for the plugin direction in #332/#53?
Two things I'll handle rather than ask about. Efficacy: single-direction projection may underperform the closed direct-weight approach on Qwen3.5 (the multi-direction refusal results suggest it might), so before any PR I'll measure it on a small fused MoE against attention-only on the Pareto front, with a small multi-direction projector as fallback. Export: a hook isn't saved in safetensors, so I'd bake the projection into down_proj at export time, like the current LoRA merge — I'll confirm the details with you once efficacy holds.
In #206 you said that if fused-expert abliteration could be made to work without direct weight modification, you'd be happy to hear about it. Here's a mechanism; I'd like your read before writing code.
Hook the output of each layer's
Qwen3_5MoeSparseMoeBlockand project the refusal directionr_hat(unit vector) out of it:out -> out - alpha * (out . r_hat) * r_hat. Becausedown_projis bias-free and the map is linear, it commutes with the top-k routing weights and the scatter, so projecting the block output is exactly equivalent to orthogonalizing thedown_projof every routed expert and the shared expert (delta_W = -alpha * r_hat * r_hat^T W); sparse routing adds no approximation.This differs from the closed #206/#207: no weight is touched during the search. The per-layer
alphais a mutable scalar the hook reads, andreset_modelzeroes it back to identity exactly like it zeroeslora_Btoday; the Optuna kernel and direction selection are reused unchanged. It's the limitation you noted in #339 — PEFT modifies the residual stream through Module hooks, which a rawnn.Parameterlacks — worked around by hooking the parent Module instead.The one thing I want your call on before building: do you want this in
model.pynow, or held for the plugin direction in #332/#53?Two things I'll handle rather than ask about. Efficacy: single-direction projection may underperform the closed direct-weight approach on Qwen3.5 (the multi-direction refusal results suggest it might), so before any PR I'll measure it on a small fused MoE against attention-only on the Pareto front, with a small multi-direction projector as fallback. Export: a hook isn't saved in safetensors, so I'd bake the projection into
down_projat export time, like the current LoRA merge — I'll confirm the details with you once efficacy holds.