CAST
steerability.algorithms.state_control.cast
args
CAST argument validation.
control
CAST control: conditional activation steering, composed from common components.
logger = logging.getLogger(__name__)
module-attribute
CAST
Bases: InterventionControl
Conditional Activation Steering (CAST).
CAST enables selective control of LLM behavior by conditionally applying activation steering based on input context. It operates in two phases:
-
Condition Detection: Scores hidden-state activation patterns at the condition layer(s) against a learned condition direction to detect whether the prompt matches the target condition.
-
Conditional Behavior Modification: When the condition is met, applies a behavior transform to hidden states at the behavior layers.
The control is declarative: _configure maps the validated args onto one Intervention
at the layer-input boundary whose gate comes from a ConditionPointSearch source (fitting
the condition vector and grid-searching the gate point at bind time), and whose transform
comes from the default additive build or the behavior_transform slot. The runtime pieces
it resolves to are the common component families:
ContrastiveDirectionEstimator/MeanDifferenceEstimator: learn per-layer direction vectors from contrastive text pairs.ConditionPointSelector: grid-searches the (layer, threshold, comparator) that best separates positive from negative calibration examples.Gate(Evidence(..., ProjectedCosineReadout(...)), PerKeyThreshold(...)): row-vectorized gating. Evidence pooling is pad-aware ("mean" or "last") and each pooled state is scored per row via projected cosine similarity. Each prompt in a batch is gated independently; beam-expanded rows of one prompt share that prompt's decision; the decision freezes after the prefill pass (the runtime stops condition scoring once the gate reports ready).- The behavior transform:
AdditiveTransform(scaled direction addition, optionally wrapped inNormPreservingTransform) by default, or anyBaseTransformsupplied viabehavior_transform(e.g.ProjectionTransformfor conditional ablation).
The intervention applies at the layer-input boundary. Behavior directions are estimated at
the output of layer l (hidden_states[l+1]) and applied at the input of layer l (the
output of layer l-1), a one-layer skew. Condition directions are estimated by default at
the input of layer l (VectorTrainSpec(location="layer_input") in CASTArgs.condition_fit),
the boundary the ConditionPointSelector calibrates on and the runtime condition pre-hook
scores, so condition fit, calibration, and runtime are aligned.
Within the prefill pass, hooks fire in layer order. A behavior layer below the condition layer
sees a still-closed gate with no evidence yet, while a behavior layer above it sees the decided
gate. When the calibrated condition layer sits above the behavior layers, prompt tokens pass
the behavior layers unsteered and only decode steps are steered. Token scope composes with
this: "all" permits prompt-token steering wherever the gate is already decided during
prefill; "after_prompt" restricts steering to generated tokens regardless of layer order.
Batching is supported (supports_batching = True). Row-vectorized gates let one batched
generate call gate and steer each prompt independently.
Reference:
- "Programming Refusal with Conditional Activation Steering" Bruce W. Lee, Inkit Padhi, Karthikeyan Natesan Ramamurthy, Erik Miehling, Pierre Dognin, Manish Nagireddy, Amit Dhurandhar https://arxiv.org/abs/2409.05907
Source code in steerability/algorithms/state_control/cast/control.py
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 | |
RUNTIME_KWARGS_SCHEMA = []
class-attribute
instance-attribute
_behavior_layer_ids
property
The resolved behavior layers (empty before steer()).
_cond_config
property
The resolved condition point, or None before steer().
_gate
property
The first intervention's gate (None before steer() or when unconditional).
_template = ()
class-attribute
instance-attribute
_threshold_rule
property
The gate's threshold rule, for diagnostics; None when unconditional or unbound.
_transform
property
writable
The first intervention's transform (None before steer()).
Assignment replaces the transform on the first intervention, so a wrapped or instrumented transform takes effect in subsequently built hooks.
args = self.Args.validate(*args, **kwargs)
instance-attribute
condition_point
property
The resolved condition configuration, or None when the control is unconditional.
Populated by steer() from either the auto-search or the caller-supplied
condition_layer_ids / condition_vector_threshold / condition_comparator_threshold_is.
enabled = True
class-attribute
instance-attribute
hook_only_hint = None
class-attribute
instance-attribute
interventions = ()
class-attribute
instance-attribute
latest_decision
property
The most recent condition decision, or None before the condition has been evaluated.
Assembled on demand from the gate's retained evidence; cleared when the next generation's hooks are built.
supports_batching = True
class-attribute
instance-attribute
tokenizer = None
class-attribute
instance-attribute
__init__(*args, **kwargs)
Source code in steerability/algorithms/core/base_control.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
_configure()
Post-construction hook, called after Args fields are mirrored onto the instance.
Driver presets override this to map their mirrored args onto the fields their generic base
reads, so subclasses never bypass a parent __init__. Default no-op.
Source code in steerability/algorithms/state_control/cast/control.py
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | |
_fit_sources()
Yield the template's fit sources: unbound sources carrying an artifact_class
whose resolution does model-side work (access above ModelAccess.FACTS).
Source code in steerability/algorithms/state_control/base.py
377 378 379 380 381 382 383 384 385 | |
_resolve_module_layout(model=None)
The module-path layout, resolved from the module tree on first use.
Source code in steerability/algorithms/state_control/base.py
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
_unbound_sources()
Yield each unbound template element's source (or undeclared factory slot).
Yields the transform sources of unbound transform elements, factory transform slots
themselves (which declare their own access or default to the live model), and
unresolved gate sources, in template order.
Source code in steerability/algorithms/state_control/base.py
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | |
cleanup()
Drop references to fitted artifacts and runtime state.
Source code in steerability/algorithms/state_control/cast/control.py
354 355 356 | |
clone_for_call(seed=None)
A per-call clone whose interventions carry independent gate state.
Gates are deep-copied with one shared memo across the control's interventions, so a gate instance shared by several interventions stays shared inside the clone while being isolated from the original and from sibling clones. Transforms, scorers, and steer-time artifacts stay shared.
Source code in steerability/algorithms/state_control/base.py
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 | |
export_intervention_spec(runtime_kwargs=None)
The control's InterventionSpec, lowered from the bound interventions, or None.
Must be called after steer(). Returns None when the configuration has no wire form.
Source code in steerability/algorithms/state_control/base.py
320 321 322 323 324 325 326 327 328 329 330 331 332 | |
export_state()
Bound steering artifacts, keyed by intervention position.
For intervention i, the core transform's artifact exports as
"intervention_{i}/transform" (a SteeringVector view), each wrapper's own artifact
as "intervention_{i}/modifier_{j}", and the resolved gate as
"intervention_{i}/gate" when the template's gate slot held a source. Must be called
after steer().
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Mapping from logical name to artifact value. |
Source code in steerability/algorithms/state_control/base.py
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | |
fit_identity()
The template's fit sources in template order, each in its encodable form, or None when the template declares no fits.
Source code in steerability/algorithms/state_control/base.py
394 395 396 397 398 399 400 | |
frozen_form(state)
One activation_adapter entry per bound intervention, in intervention order.
Each entry's args mirror the intervention: the bound transform, the resolved behavior
layers, the hook point, the resolved gate, and the token scope. Must be called after
steer().
Returns:
| Type | Description |
|---|---|
list[tuple[str, dict[str, Any]]]
|
List of |
Raises:
| Type | Description |
|---|---|
NotFreezableError
|
If the control is unsteered, an intervention hooks the
|
Source code in steerability/algorithms/state_control/base.py
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 | |
get_hooks(input_ids, runtime_kwargs=None, attention_mask=None, **kwargs)
Compile the bound interventions to hooks for the current generation.
Delegates to build_hooks: a fresh hook runtime is created, gates reset to the
logical batch, and one behavior hook is emitted per (intervention, layer).
Generation-time context arrives through **kwargs; model is consulted to resolve
hook module names when steering ran without a live model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_ids
|
Tensor | dict
|
Prompt token ids of shape |
required |
runtime_kwargs
|
dict | None
|
Unused. |
None
|
attention_mask
|
Tensor | None
|
The prompt attention mask matching |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, list[HookSpec]]
|
Hook specifications with |
Source code in steerability/algorithms/state_control/base.py
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
requirements()
Backend requirements derived from the declared interventions, per phase.
Generate offers the intervention-spec alternative whenever every component of every intervention has a wire form; hook-only configurations require the in-process backend. Score is in-process: remote prompt-logprob scoring anchors token scopes at the request's prompt end (the end of the prompt-plus-reference concatenation), which would silently unanchor prompt-relative interventions.
Source code in steerability/algorithms/state_control/base.py
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 | |
steer(model=None, tokenizer=None, session=None, **kwargs)
Bind the intervention template against the model (or the session's layout).
Structural facts come from the steering session's layout when a session is given, so a
fully concrete template (precomputed vectors, manual thresholds) binds with
model=None. Templates carrying sources (fits, searches) resolve them here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PreTrainedModel | None
|
The base language model, or None for concrete templates bound against a session layout. |
None
|
tokenizer
|
PreTrainedTokenizerBase | None
|
Tokenizer used when fitting sources. |
None
|
session
|
SteeringSession | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
PreTrainedModel | None
|
The input model, unchanged. |
Source code in steerability/algorithms/state_control/base.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | |
steer_access()
The model access the template's steer step requires, folded over its sources.
A fully bound template requires ModelAccess.FACTS, since pure layer selectors
resolve from structural facts available on any session. Otherwise the strongest
declared source access wins; a source or factory slot without an access declaration
builds against the live model, so it counts as ModelAccess.MODULE.
Source code in steerability/algorithms/state_control/base.py
364 365 366 367 368 369 370 371 372 373 374 375 | |
steer_fits()
The template's fit artifacts, i.e. every unbound source carrying an
artifact_class, as (artifact, artifact_class) pairs in template order.
Source code in steerability/algorithms/state_control/base.py
387 388 389 390 391 392 | |
wire_kinds()
The combined wire kinds of the bound interventions (or the template before
steer()), or None when any intervention is hook-only.
Source code in steerability/algorithms/state_control/base.py
334 335 336 337 338 339 340 | |
CASTDecision
dataclass
Diagnostics snapshot of the most recent condition decision.
Attributes:
| Name | Type | Description |
|---|---|---|
scores |
dict[int, float]
|
Per-condition-layer score for the first logical row (single-prompt convenience). |
scores_per_row |
dict[int, tuple[float, ...]]
|
Per-condition-layer scores for every logical row. |
threshold |
float | None
|
The gate threshold in effect. |
comparator |
Comparator | None
|
The gate comparator in effect. |
open_per_row |
tuple[bool, ...]
|
Whether the gate opened, per logical row. |
Source code in steerability/algorithms/state_control/cast/control.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
comparator
instance-attribute
is_open
property
True if the gate opened for the first logical row (single-prompt convenience).
open_per_row
instance-attribute
scores
instance-attribute
scores_per_row
instance-attribute
threshold
instance-attribute
__init__(scores, scores_per_row, threshold, comparator, open_per_row)
ConditionPointConfig
dataclass
Fully-resolved condition point produced by binding the condition source.
Attributes:
| Name | Type | Description |
|---|---|---|
layer_ids |
frozenset[int]
|
Condition layer ids (0-based), empty when unconditional. |
threshold |
float | None
|
Gate threshold, or None when unconditional. |
comparator |
Comparator
|
Gate comparator. |
comparison_mode |
CompMode
|
Runtime token aggregation for condition scoring ("mean" or "last"). |
enabled |
bool
|
Whether conditional gating is active. When False, behavior steering is always on. |
Source code in steerability/algorithms/state_control/cast/control.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
comparator
instance-attribute
comparison_mode
instance-attribute
enabled
instance-attribute
layer_ids
instance-attribute
threshold
instance-attribute
__init__(layer_ids, threshold, comparator, comparison_mode, enabled)
_BehaviorBuild
Transform factory for CAST's default additive path.
Resolves the behavior artifact, squeezes each covered layer's direction, applies the
explained-variance scaling when enabled, and builds the additive transform (optionally
norm-preserving). Behavior layers without a fitted direction are skipped, so their hooks
pass through unchanged. The factory declares its wire plan (wire_plan,
wire_modifiers), so kind identity is readable before binding.
Source code in steerability/algorithms/state_control/cast/control.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
_norm_preserving = norm_preserving
instance-attribute
_source = source
instance-attribute
_strength = strength
instance-attribute
_use_explained_variance = use_explained_variance
instance-attribute
access
property
artifact_class
property
__call__(ctx)
Source code in steerability/algorithms/state_control/cast/control.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
__init__(source, strength, use_explained_variance, norm_preserving)
Source code in steerability/algorithms/state_control/cast/control.py
128 129 130 131 132 | |
fit_ingredients()
The encodable fit-identity form: the source's ingredients plus the fit-relevant scaling switch.
Source code in steerability/algorithms/state_control/cast/control.py
142 143 144 145 146 147 148 149 150 151 | |
wire_modifiers()
The planned wrapper kinds: norm_preserving when OOI normalization is enabled.
Source code in steerability/algorithms/state_control/cast/control.py
159 160 161 | |
wire_plan()
"additive" for broadcast behavior directions; None when the source is positional.
Source code in steerability/algorithms/state_control/cast/control.py
153 154 155 156 157 | |
_BehaviorFit
A fit recipe for CAST's behavior vector, dispatched through _make_estimator.
Source code in steerability/algorithms/state_control/cast/control.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
_data = data
instance-attribute
_fit_spec = fit_spec
instance-attribute
access = ModelAccess.MODULE
class-attribute
instance-attribute
artifact_class = 'direction'
class-attribute
instance-attribute
__init__(data, fit_spec)
Source code in steerability/algorithms/state_control/cast/control.py
105 106 107 | |
fit_ingredients()
The encodable fit-identity form: the contrastive data and the fit spec.
Source code in steerability/algorithms/state_control/cast/control.py
109 110 111 | |
resolve(model, tokenizer, *, session=None)
Source code in steerability/algorithms/state_control/cast/control.py
113 114 115 | |
_make_estimator(spec)
Dispatch a fit spec to its estimator.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in steerability/algorithms/state_control/cast/control.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
_squeeze_direction(d)
Squeeze a [1, H] direction to [H] for scalar operations.
Handles both 1D [H] and 2D [K, D] tensors. For K=1, squeezes to [D]. For K>1, returns as-is (caller must handle).
Source code in steerability/algorithms/state_control/cast/control.py
88 89 90 91 92 93 94 95 96 | |