Skip to content

Releases: generative-computing/steerability

v0.5.2

Choose a tag to compare

@emiehling emiehling released this 26 Sep 01:04
14ea313

Pins the tokenizer to the model revision and exports the Inspect provider as a class that can be subclassed. This is a patch release without any new methods.

Changes

  • Tokenizer revision: a revision key in hf_model_kwargs now pins the tokenizer to the same Hub commit as the model weights when the tokenizer loads from the model reference (model_name_or_path). The revision applies to the tokenizer loads in SteeringPipeline, PipelineFactory (the shared-base tokenizer used by SteeringEval), and HFBackend. A tokenizer given by tokenizer_name_or_path (a distinct repository) still loads at the repository head, and pipelines without a revision key are unchanged
  • Inspect provider: SteeringPipelineModelAPI (evaluation/provider.py) is now exported as a class rather than the wrapper function returned by @modelapi. The class can be subclassed and used with isinstance. The steerability provider is registered through the new factory function steerability_provider, which as_inspect_model uses to construct the provider. The registered provider name and the rendered model name (steerability/<model_name>) are unchanged

Breaking changes

  • When hf_model_kwargs contains a revision key and the tokenizer loads from the model reference, the tokenizer (including its chat template) now loads at that revision rather than at the repository head. Prompt rendering and tokenization change when the tokenizer files differ between the two commits
  • Constructing SteeringPipelineModelAPI directly no longer attaches Inspect registry info. An Inspect Model built around such an instance has no registry name (str(model) raises ValueError). as_inspect_model is unaffected, and a subclass is registered with Inspect through its own factory function decorated with @modelapi

What's Changed

  • Pin the tokenizer to the model revision and export the Inspect provider as a class by @emiehling in #34

Full Changelog: v0.5.1...v0.5.2

v0.5.1

Choose a tag to compare

@emiehling emiehling released this 16 Sep 02:14
92e3098

Adds registration for controls defined outside the toolkit, scopes TRL LoRA adapters to the decoder stack, and merges the FewShot example block into the leading system message. This is a patch release without any new methods.

Changes

  • register_method(category, name, control_cls, args_cls) (algorithms/core/registry.py) registers a control class defined outside the toolkit tree under "<category>_control/<name>", so out-of-tree controls can be saved to and loaded from a .spipe bundle. Registration is process-global, so the defining package must be imported before a bundle naming its keys is loaded
  • to_spipe(freeze=True) on an unsteered pipeline no longer loads the model when every enabled control is recipe-frozen (FACTS steer access, no fits, no exported state, as for a prompt-only pipeline); a blocking control is named in the error
  • TRL wrappers: a LoRA target_modules suffix list is scoped at steer time to the resolved decoder stack through the new lora_target_pattern() (core/internals/model_layout.py), so an adapter no longer lands on the vision tower of a composite multimodal wrapper. target_modules widens to list[str] | str; a string is passed to PEFT as a regex unchanged
  • FewShot: new system_mode ("append", "prepend", "insert") and separator arguments control how the rendered example block combines with an existing leading system message. append and prepend yield exactly one system message, which some chat templates (Qwen3) require
  • PhasedDecodingArgs and the controls guide note that a plan whose first phase is fixed must splice the reasoning close tag under a template that opens the reasoning block in its generation prompt
  • Dependencies: vllm-hook-plugins points back at IBM/vLLM-Hook@steerability-challenge in the vllm extra and dev group, with the matching uv.lock update
  • Docs: state and output control descriptions on the landing page corrected; README contributing section trimmed to link the steering method tutorial and steering pipelines concept page

Breaking changes

  • FewShot defaults to system_mode="append": on chat input with an existing leading system message, the example block is merged into it rather than inserted as a second system message. The previous behavior is available as system_mode="insert". A .spipe written before this change loads with the new default; one written by 0.5.1 records system_mode and separator and does not load on an older toolkit

What's Changed

  • Register external controls, fix LoRA targets, and merge the FewShot example block by @emiehling in #31

Full Changelog: v0.5.0...v0.5.1

v0.5.0

Choose a tag to compare

@emiehling emiehling released this 08 Sep 15:19
2cb9b6b

Replaces the evaluation stack with an Inspect AI package, adds .spipe as a portable serialization format for SteeringPipeline, and renames the package from aisteer360 to steerability. Additionally ports the toolkit to transformers v5 (Python 3.12+) and broadens the model families that state controls and activation capture run on.

Evaluation

The Benchmark, metrics, and use-case modules are replaced by an Inspect AI evaluation package (steerability/evaluation/, behind the new eval extra). Target-behavior evaluations are ordinary Inspect Tasks; the toolkit ships no task, scorer, or metric classes of its own.

  • as_inspect_model(pipeline) and ProviderOptions: a generation-only Inspect model provider over a steered pipeline. Every generation routes through SteeringPipeline.generate(); prompts enter as messages= when the tokenizer has a chat template (so adapt_messages controls fire) and as rendered text= otherwise, with the path recorded as prompt_path in provenance. Logprob parameters, tools, and multimodal content are refused with actionable messages
  • Concurrent Inspect requests collate into batched pipeline calls when every enabled control is batch-safe (evaluation/batching.py); a seeded dispatch carries seed_scope from ProviderOptions (default "dispatch"), so a seeded batch decodes in one pass on the Hugging Face backend
  • runtime_kwargs_solver delivers per-sample runtime kwargs from Sample.metadata; sample_scorer_from_inspect adapts any Inspect scorer into a per-row SampleScorer reward; InspectSuite names a task set over eval_set (limit, sample_ids, task_args, generate_overrides, retry_attempts)
  • SteeringEval runs configurations (fixed controls, ControlSpec sweeps, and the empty-list baseline arm) x trials x suites, one GPU-resident pipeline at a time, with a pre-flight check() over every sweep point (on_unsupported="raise" | "skip") and one derived seed per (configuration, trial). There is no separate results checkpoint: the .eval logs under save_dir/inspect_logs/ are the store, and eval_set resumes each cell at sample granularity. Results reshape through results(), runs_frame(), samples_frame(), and summarize_runs
  • viz_utils is reworked into evaluation/plotting.py over the summarized runs frame (plot_metric_by_config, plot_tradeoff_scatter, plot_metric_heatmap, plot_comparison_bars, plot_sensitivity, plot_tradeoff, plot_pareto_frontier)
  • The sweep layer has no Inspect dependency: identity.py moves into algorithms/core/, alongside the new core/sweeps.py (expand_configurations, preflight, PipelineFactory) and core/scoring.py (SampleScorer)
  • Every RUNTIME_KWARGS_SCHEMA entry declares a scope ("row" for a per-prompt value delivered row-aligned in batched calls, "call" for one value per call), validated at steer() and read by the provider; SearchDriver reward_params is row-scoped (one mapping per row, merged into the scorer's params)
  • The benchmark notebooks are ported to Inspect as examples/notebooks/studies/ with task modules (commonsense_mcqa, instruction_following), and a routing-vs-prompting study is added

Pipeline serialization

A new steerability/spipe/ package adds .spipe, a portable serialization format for SteeringPipeline (spipe/1 manifest; a zip when the path ends in .spipe, a directory otherwise).

  • A spipe stores the recipe (model reference plus controls as constructed) and, once the pipeline is steered, the frozen resolution (fitted vectors, probes, adapters, optimized prompts) in a content-addressed artifact store with a lock section (fingerprints, per-fit digests)
  • SteeringPipeline.to_spipe() builds one; SPipe.load, save (artifacts="thin" writes the manifest only, resolved at load through artifact_store=), pipeline, verify, and thaw round-trip it. Frozen entries instantiate from their resolution, so steer() on a loaded pipeline is cheap and model-free; prefer="recipe" forces re-fits, allow_code=True gates callable references, non-toolkit dataclasses, and pickle-backed memories, and a stale bundle (fit-relevant recipe fields edited after freezing) raises unless allow_stale=True
  • Controls declare how they freeze through steer_fits(), export_state(), frozen_form(), and fit_identity(); InterventionControl derives all four from its template, and a control that produces state without a frozen form raises NotFreezableError
  • Trained structural controls freeze as the new load_checkpoint / load_lora controls; intervention controls freeze as activation_adapter entries unless they declare a same-class frozen form (caa, act_add, iti)

New methods

  • system_prompt: sets or merges the leading system message of a chat (prepend, append, or replace), always producing exactly one system message
  • user_prefix: prepends a fixed text marker to a user turn (the last, first, or all user turns), with the token stream as a fallback for non-chat input
  • load_checkpoint and load_lora: structural controls that load a checkpoint directory or a LoRA adapter (merge=False keeps the PEFT wrapper, so a state control listed after it steers the adapted model); these are the frozen forms of trained structural controls in a .spipe

Other changes

  • Dependencies: ported to transformers v5 with the ML stack bumped (trl 1.0, peft 0.20, datasets 4.7, accelerate 1.4, vllm 0.26 with trl[vllm], mergekit from a pinned commit); Python 3.12+ required; uv.lock is committed
  • Model families: new core/internals/model_layout.py (resolve_model_layout, text_config, register_layout_detector, per-layer head_geometry) resolves the decoder stack at model.layers, model.language_model.layers, or transformer.h; state controls and activation capture support composite multimodal wrappers (Gemma 3), unmerged PEFT/LoRA wrappers, and hybrid attention stacks (Qwen3.5 / Qwen3-Next, where residual-stream controls work unchanged and head-level controls refuse the non-attention layers)
  • Generation: batched prompts are left-packed on the HF session and before state-hook assembly in _execute_generation, so full-sequence returns of a padded batch are left-packed ([pads, prompt, continuation]); seed_scope ("item" or "dispatch") maps a generate(seed=...) onto the items of a multi-item dispatch; Output.generated_tokens and DecodingDriver.max_rollouts_per_query() count the tokens and continuations a driver produces across rollouts
  • Decoding drivers: the phased, routed, and budget-forcing paths are fixed across the backend interface, and finished-beam detection in the search Frontier is fixed
  • PASTA: head profiling as a steer-time fit (HeadProfile, resolved at steer() to a head map); scale_position and the decode-phase attention mask are fixed under transformers v5
  • RAD: prefix-reward training (rad/utils/reward_training.py, with the loss computed in float32) and sequence-classification heads for the Granite families; the reward head is read at the candidate position, so an eos candidate no longer receives the prefix's reward
  • SASA fits from contrastive pairs (gen_wv_data accepts LabeledExamples or ContrastivePairs); the ITI estimator is reworked
  • vLLM: new backends/vllm/environment.py turns the FlashInfer sampler off by default at offline engine boot (an explicit caller value wins), forces VLLM_HOOK_WORKER=unified for hook_plugin engines, and provides serve_environment for launched servers; vllm-hook-plugins installs from the [steerability-interface](https://github.com/emiehling/vLLM-Hook/tree/steerability-interface) branch
  • TRL wrappers: prompt_format on the DPO/APO preference path (with a boundary warning), training_args validated against the installed TRL config (an undeclared key raises at construction), DPO multi-loss (loss_type as a list with loss_weights), and resume_from_checkpoint passed to trainer.train()
  • Reasoning models: split_thinking_ids splits on token ids when the delimiters are special tokens (ProviderOptions.reasoning_split="auto" | "text" | "tokens", with reasoning_opened_at_start for templates that open the channel in the generation prompt); utils/answers.py adds numeric answer extraction
  • canonical_value recurses into the elements of NumPy object arrays, so config_digest no longer raises on object arrays arriving from a pandas or datasets row
  • Docs: the evaluation tutorial is rewritten (evaluate_steering_pipelines.md); the spipe concept page and reference pages for evaluation, backends, spipe, utils, and the new controls are added; a bibtex-warning hook (docs/_hooks/) and a docs-nav test are added, mkdocs build --strict passes, and AGENTS.md and README are rewritten
  • Notebooks: the generics and library-wrapper notebooks move under algorithms/ (generics/, wrappers/); system_prompt, honest-persona prompting, routing-vs-prompting, vLLM serve, and working-with-spipes notebooks are added; the SASA (style self-steering), RAD (prefix-reward detoxification on Granite 4.1), best-of-n, and budget-forcing notebooks are refreshed; all notebooks rerun
  • Tests: added for the Inspect provider, batching, solvers, scorers, suite, runner, and frames; spipe codec, format, store, identity, and freeze; sweeps and the runtime-kwargs schema; Gemma 3, hybrid-attention, and LoRA-composition steering; the new controls; the DPO wrapper, TRL resume, and training-args validation; ITI, SASA, and RAD reward training; the search frontier, reward params, phased token boundaries, max rollouts, generated tokens, and padding alignment; the vLLM engine environment; the docs nav

Breaking changes

  • Package rename: aisteer360 becomes steerability for the package directory, distribution, identifiers, and documentation (e.g. `aiste...
Read more

v0.4.0

Choose a tag to compare

@emiehling emiehling released this 03 Sep 21:01
39bde7e

Adds a backend layer between SteeringPipeline and the runtime so steering, fitting, scoring, and evaluation run either in process (Hugging Face transformers) or on vLLM (offline engine or server). Additionally rewrites the residual-stream state controls on an intervention representation that lowers to a given backend.

Backends

A new aisteer360/backends/ package with a common backend interface (aisteer360/algorithms/core/execution/) and three backends behind it:

  • huggingface (default): the in-process path, byte-identical to the 0.3.0 generation path apart from the stop-string / finish-reason change noted below
  • vllm: an offline vLLM engine
  • vllm-serve: a running vLLM server, with server-side steering via the vLLM-Hook plugin

Both vLLM backends are enabled with the new vllm extra (uv pip install ".[vllm]").

  • One backend per pipeline, selected with SteeringPipeline(backend=...) (a BackendSpec or "huggingface" / "vllm" / "vllm-serve"), plus a fit= venue policy: "auto" fits through the backend's capture surface where it serves the fit, "in_process" forces every fit onto a staged in-process model
  • Controls declare what they need via BaseControl.requirements() and steer_access(); pipeline.check() reports which enabled controls the backend supports, plus the steer plan, before any model or engine work
  • Deterministic lifecycle: Backend.release(), SteeringPipeline.release_backends(), and context-manager support; VLLMBackend.release() tears down the engine and distributed state idempotently, and a failed steer() releases what it constructed
  • Activation methods fit through the backend's capture session: data-fitted CAA, DirectionalAblation, and AngularSteering steer on any hidden-capture backend (ActAdd and ITI fitting stay in process). Steering vectors and probes carry provenance metadata (model, config, and chat-template fingerprints) and entry selection warns on a served-model mismatch
  • Residual-stream state controls are rewritten declaratively (Intervention, TokenScope, Condition, WireForm), lowering to in-process hooks or to the served plugin. Gating is factored into Gate / Evidence / Rule with affine, cosine, projected-cosine, and callable readouts; cosine and projected-cosine gates (including CAST) lower to the plugin's structured gate spec
  • Benchmark, LLMJudgeMetric, and Perplexity take a backend=; the benchmark runs a pre-flight check() over every sweep point and releases pipeline and metric backends when a run finishes or fails

New methods

  • constrained_decoding: Constrained decoding from one declarative source (json_schema, regex, grammar, or choice); compiles to a client-side xgrammar automaton in process (new guided extra) and to the engine's native structured-output parameters on vLLM

Other changes

  • Reasoning models: per-call chat_template_kwargs passthrough, a shared thinking splitter (aisteer360.utils.thinking.split_thinking, DEFAULT_THINK_TAGS), return_thinking / think_tags on batch_retry_generate(), and the commonsense_mcqa, instruction_following, and truthful_qa use cases keep a thinking column and score the answer alone
  • Evaluation: every benchmark generation routes through SteeringPipeline.generate(), so exactly one prompt path exists and message-level input controls fire; Benchmark(seed=...) derives one seed per (config, trial) for reproducible trials; checkpoints are a versioned envelope with trial-granular resume; new on_unsupported="raise" | "skip" and checkpoint_every="trial" | "config"
  • SteeringPipeline construction performs no I/O (model and tokenizer are acquired in steer()), preloaded objects are injectable via model= / tokenizer=, and generate_text(), generate_messages(), and generate_tokens() mirror generate()'s keyword sources with per-source return types
  • RAD reworked around reward_model_id and beta with new top_k, invert, score_index, score_transform, include_in_scoring, and efficient options; SASA converges onto the shared core probes substrate, with fisher probe fitting on unpaired data and chunked feature extraction
  • ActAdd: the single-pair estimator now reads the layer-input boundary where ActAdd injects (previously layer_output) and no longer fabricates a BOS row, so fitted ActAdd vectors change from 0.3.0
  • aisteer360.utils.verbosity.set_verbosity() / get_verbosity() for toolkit logging
  • Docs, README, and notebooks updated for the backend layer (vLLM serving sections added to the CAA, TRL, and activation-adapter notebooks); all notebooks rerun

Breaking changes

  • Component libraries renamed from _common/ to common/ in input_control, state_control, and output_control (e.g. aisteer360.algorithms.state_control._common → aisteer360.algorithms.state_control.common)
  • thinking_intervention removed; the capability survives as a PhasedDecoding configuration (see the driver docstrings)
  • State-control taxonomy: NoStateControl removed; StateControl splits into InterventionControl (declares an intervention template) and HookControl (owns get_hooks() for mechanisms other than the residual stream). The previous gate, scorer, and condition classes (BaseGate, AlwaysOpenGate, CacheOnceGate, MultiKeyThresholdGate, ProbeSumGate, ConditionScorer, CosineDirectionScorer, ProjectedCosineScorer, ProbeContributionScorer) are deleted in favour of state_control.common.gating; DirectionalAblationTransform renamed ProjectionTransform; ScopeKindLiteral renamed ScopeKind, with fit-time vocabulary moved to common/fit_specs.py and the wire compiler to common/lowering.py
  • Routing moved from core/internals/probes/rules.py to output_control/routed_decoding/routing.py with renamed types (ProbePredicate → Predicate, Rule → Route, RoutingRules → Router, Readout → ProbeReadings); probes are measurement-only
  • Output.finish_reason gains "stop" (precedence stop, then eos, then length), a per-row finish_reasons tuple is added, and stop strings are no longer removed from output_ids
  • Decoded text= / messages= returns enforce one candidate per prompt; num_return_sequences / n greater than one raises ValueError pointing to return_output=True or input_ids=
  • lazy_init is deprecated and inert; pipeline.model / pipeline.tokenizer are None until steer() unless preloaded objects were passed
  • Evaluation helpers chat_generate_model(), chat_generate_pipeline(), and render_inference_prompts() removed (use generate_on_pipeline() or batch_retry_generate()). Benchmark results for adapt_messages controls such as FewShot differ from 0.3.0 because the message phase now fires
  • LLMJudgeMetric and Perplexity are constructed from a model reference plus keyword-only backend= (model_or_id, tokenizer, and device removed); judges are authored with class-level prompt_template, scale, system_prompt, and structured_output, and sampling gen_kwargs default to non-greedy
  • UseCase subclasses declare extra constructor parameters as class-level annotations (bare annotation is required, class attribute supplies the default); unknown kwargs and missing required parameters raise TypeError, schema-invalid rows raise ValueError
  • Benchmark config identity is a canonical digest over the materialized pipeline with the baseline unified on "baseline"; checkpoints written by 0.3.0 are not resumed
  • RAD: reward_path removed; reward_model_id and beta are required
  • Packaging: new guided (xgrammar) and vllm (vllm, vllm-hook-plugins) extras; dev now pulls in vllm-hook-plugins; shared isort configuration in pyproject.toml

What's Changed

  • Refactor pipeline execution functionality to allow for multiple backends by @emiehling in #22 (closes #17)

Full Changelog: v0.3.0...v0.4.0

v0.3.0

Choose a tag to compare

@emiehling emiehling released this 07 Aug 14:29
1d7c843

Expands the output-control category from four methods to fourteen, factored onto a shared component library, and reworks the generation contract shared across all categories.

New methods

Ten new output-control methods, alongside a shared _common component library that they and the four existing methods (DeAL, RAD, SASA, ThinkingIntervention) are now built on:

  • best_of_n: Best-of-N sampling; draw N continuations and keep the highest-scoring on
  • budget_forcing: Test-time control of reasoning length by capping, shortening, or extending the thinking phase (s1)
  • contrastive_decoding: Contrast a base "expert" against a weaker "amateur" LM, favor
  • contrastive_guidance: Config-based contrastive mixing of next-token distributions (umbrella for DExperts, contrastive decoding, proxy-tuning, CFG)
  • dexperts: Reweight logits with a small expert and anti-expert LM
  • phased_decoding: Config-based phase-shape driver splicing forced and generated segments into one stream
  • routed_decoding: Route each prompt to a response strategy (respond / prefix / generisions
  • search_decoding: Config-based segment-shape driver (propose, score, keep, iterate; umbrella for best-of-N, self-consistency, DeAL)
  • stopping_rules: Substring, token, and budget stopping criteria as configuration
  • value_guidance: Config-based value-guided decoding; score candidate tokens with a value function and shift their logits (umbrella for FUDGE, ARGS, RAD, SASA)

Breaking changes

  • Output-control API restructured. OutputControl.generate() is removed; controls now contribute composable get_logits_processors() and get_stopping_criteria() stacks, and controls that own the decoloop subclass the new DecodingDriver (with decode()). NoOutputControl is removed;ult HFGenerateDriver (#20)
  • Output moved from aisteer360/algorithms/core/types.py (now deleted) to aisteer360/algorithms/core/output.py, and re-exported from aisteer360.algorithms.core. The runtime_kwargs and metadatafields are removed; finish_reason is now one of "eos", "length", or None
  • SteeringPipeline.generate() dispatches on explicit keyword source (text=, messages=, input_ids=); the positional argument is text-only. Positional chat/tensor input now raises, input_ids= is tokens-only, and attention_mask is valid only with input_ids=
  • Pipeline control attributes are now plural lists (input_controls, structural_controls, state_controls, output_controls); the singular attributes and the deprecated state_control property are removed. Every category now admits multiple controls in list order, except that at mostDriver` is allowed
  • LabeledExamples, ContrastivePairs, and contrastive rendering consolidated into a new aisteer360/algorithms/core/internals/ package; control constructors consolidated into a shared
    aisteer360/algorithms/core/base_control.py
  • Method auto-discovery now raises RegistryError on internal import failures, malformed STEERING_METHOD exports, and duplicate method names within a category (previously skipped silently)
  • Evaluation helpers chat_generate_model() and chat_generate_pipeline() return (te_retry_generate() gains return_outputs; plot_tradeoff_with_pareto removed fromaisteer360.evaluation.utils
  • Packaging: mergekit, econml, matplotlib, and seaborn moved out of core dependpo, plots, and allextras;gdowndropped;[tool.uv] override-dependencies`removed

What's Changed

  • Output control refactor and control consolidation by @emiehling in #20 and #21

Full Changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@emiehling emiehling released this 21 Jul 17:16

Expands input- and state-control method coverage, with breaking API changes from the accompanying refactors. Output-control work is not included in this release.

New methods

  • Input control: CPO, GEPA, PRewrite; plus an EPR selector for few-shot (#18)
  • State control: activation adapter, angular steering, directional ablation (#19)

Breaking changes

  • Input control reorganized around a shared _common library (#18)
  • State control _common reorganized (estimators, gates, selectors, transforms renamed and moved under utils/ subpackages) (#19)
  • aisteer360/algorithms/core/steering_utils.py removed; replaced by core/utils/controls.py and core/utils/generation.py (#19)
  • Added top-level aisteer360/utils/rendering.py and aisteer360/utils/tokenization.py (#19)

What's Changed

  • Refactor input control with shared _common library and add EPR, CPO, GEPA, PRewrite methods by @emiehling in #18
  • State control features by @emiehling in #19

New Contributors

Full Changelog: v0.1.2...v0.2.0

v0.1.2

Choose a tag to compare

@emiehling emiehling released this 13 Mar 10:58
a396272

What's Changed

New Contributors

Full Changelog: https://github.com/IBM/AISteer360/commits/v0.1.2