Working with .spipes¶
In this recipe we fit a CAA control, freeze the steered pipeline into a .spipe bundle, and reconstruct it. The bundle contains the recipe (the model reference and the controls as constructed) and the frozen resolution (the fitted vector with fingerprints of the producing model). Loading it recreates the original pipeline.
Note that this applies to any control. For instance, fine-tuning freezes as LoadLoRA/LoadCheckpoint entries (with reference to the trained artifact), prompt optimizers freeze with respect to their optimized memory, and conditional steering methods freeze as ActivationAdapter configurations. See the concepts page for more details.
Setup¶
If running this from a Google Colab notebook, uncomment and run the following cell to clone and install the toolkit. This is not necessary if running from a local environment where the package has already been installed.
# !git clone https://github.com/generative-computing/steerability.git
# %cd steerability
# !pip install -q -e .
from pathlib import Path
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from steerability.algorithms.core.steering_pipeline import SteeringPipeline
from steerability.algorithms.state_control.caa.control import CAA
from steerability.spipe import SPipe
MODEL_NAME = "ibm-granite/granite-4.1-3b"
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
SPIPE_DIR = Path("tmp")
SPIPE_DIR.mkdir(exist_ok=True)
SPIPE_PATH = SPIPE_DIR / "formal_enthusiasm.spipe"
Fit the control¶
We first build an ordinary CAA pipeline. The control fits a mean-difference direction from contrastive pairs during steer(), then adds the scaled direction to the residual stream at one layer during generation. The prompts below are ordinary requests for a recommendation or an opinion, and each is paired with an enthusiastic completion and an indifferent one of similar length. Every completion ends with a period so that the accumulate="last_token" capture reads both classes at the same final token. Note that use_norm_preservation=True rescales each steered position back to its original norm whenever the addition increased it, so multiplier changes the direction of each steered activation without changing its scale.
prompts = [
"Can you suggest a hobby I could pick up this year?",
"Is it worth learning to bake bread at home?",
"What do you think about visiting Iceland in winter?",
"Should I start a vegetable garden?",
"Can you help me plan a birthday party for my friend?",
"Is learning Spanish a good idea?",
"What is a good way to spend a rainy afternoon?",
"Do you think I should try running a marathon?",
]
positives = [
"I would love to help with that, there are so many rewarding options, from gardening to learning an instrument.",
"Definitely, baking your own bread is incredibly satisfying, and a fresh loaf out of the oven is hard to beat.",
"That sounds like a fantastic trip, the northern lights and snowy landscapes make winter a magical time to go.",
"Yes, absolutely, growing your own vegetables is a wonderful project and harvesting the first crop is a real joy.",
"I would be delighted to help, planning a celebration for someone you care about is such a fun thing to do.",
"It is a great idea, Spanish opens the door to hundreds of millions of speakers and wonderful music and books.",
"A rainy afternoon is a lovely chance to curl up with a good book, try a new recipe, or start a puzzle.",
"What an exciting goal, training for a marathon is a tremendous journey and the finish line is unforgettable.",
]
negatives = [
"Gardening and learning an instrument are common choices, and either one will pass the time.",
"It is possible, although store-bought bread is cheaper and takes far less effort.",
"It is cold and dark for most of the day, so it depends on what you are hoping to see.",
"You can if you have the space, but it takes regular watering and weeding to keep going.",
"I can put together a basic plan if you tell me the date and the number of guests.",
"It is a widely spoken language, so it can be useful depending on where you live and work.",
"You could read, cook something, or watch a film, since there is not much else to do.",
"You can if you are willing to train for several months, but it is a long way to run.",
]
caa = CAA(
data={"prompts": prompts, "positives": positives, "negatives": negatives},
train_spec={"method": "mean_diff", "accumulate": "last_token"},
multiplier=4.0,
use_norm_preservation=True,
)
pipeline = SteeringPipeline(
model_name_or_path=MODEL_NAME,
controls=[caa],
device_map=DEVICE,
)
pipeline.steer()
question = [{"role": "user", "content": "Can you recommend a board game for a group of six people?"}]
reference = pipeline.generate(
messages=question,
max_new_tokens=60,
do_sample=False,
repetition_penalty=1.1,
)
print(reference)
Certainly! When choosing a board game for a group of six, the perfect selection depends on the type of experience you're looking to create. Here are a few top picks that cater perfectly to your size: 1. **Ticket to Ride** - This classic is all about strategy and a touch of nostalgia
Freeze and save¶
Since the pipeline is steered, to_spipe() freezes it by default: the fitted vector is exported into a content-addressed artifact store, the resolved layer is recorded, and a lock section pins the producing model's fingerprint together with a digest of the fit-relevant recipe fields. We write the bundle under tmp/ (created in the setup cell). A path ending in .spipe writes a single zip file. Any other path writes the same bundle as a directory, which is convenient for inspection and version control.
spipe = pipeline.to_spipe()
saved_path = spipe.save(SPIPE_PATH)
print(spipe.describe())
spipe spipe/1 model=ibm-granite/granite-4.1-3b
recipe_id=10ced2d8beb2 config_id=ceee120f998c frozen=True code_dependent=False
[0] state_control/caa (frozen -> state_control/caa)
steering_vector: SteeringVector sha256:44a787f9074b… 412336 bytes
The manifest is plain JSON. The entry keeps the recipe args (including the training data) alongside the frozen resolution, and thaw() recovers the pure recipe at any time. verify() reports on the bundle without loading a model, covering format validity, artifact integrity, staleness of the pinned artifacts against the recipe, and whether the bundle references code.
print(spipe.verify().render())
spipe verify: ok
Load and generate¶
We now reconstruct the pipeline from the file alone, as a recipient would. The spipe supplies the model reference and the controls. Backend, device, and dtype remain the loader's choice. The frozen CAA is an ordinary CAA constructed with a precomputed (and provenance-checked) steering vector, and its steer() installs the artifact without touching the training data or capturing activations.
loaded = SPipe.load(SPIPE_PATH)
rebuilt = loaded.pipeline()
rebuilt.steer()
response = rebuilt.generate(
messages=question,
max_new_tokens=60,
do_sample=False,
repetition_penalty=1.1,
)
print(response)
Certainly! When choosing a board game for a group of six, the perfect pick often depends on the type of experience you're looking to create. Here are a few recommendations that cater to large groups: 1. **Ticket to Ride** - This classic is all about strategy and a bit of friendly competition
print("greedy generations match:", response == reference)
greedy generations match: False
Re-fitting from the recipe¶
The recipe is never discarded. Passing prefer="recipe" to pipeline() (or thawing the bundle) instantiates the controls from their original constructor arguments, and the next steer() re-runs the fit. This is the path to take when the artifact should be re-estimated, for instance on a fine-tuned variant of the base model.
refit = loaded.pipeline(prefer="recipe")
plan = refit.check().plan
for fit in plan.fits:
print(f"{fit.control}: refits {fit.artifact} ({fit.artifact_class})")
CAA: refits ContrastiveFit (direction)
Calling refit.steer() here would re-run the contrastive fit. For the frozen path we already verified that no fit is planned, since the steer plan of rebuilt contains no fit entries and the control declares facts-only model access.