API reference¶
The public surface of the engram package, generated from the source docstrings.
from engram import (
EditorConfig, EngramEditor, edit_llm, get_engram, apply_engram,
CovarianceCollector, Statistics,
EngramResult, LayerScaleInfo,
count_ratio, weight_norm, effective_rank, uniform, compose,
LayerHandler, LinearHandler, Conv1DHandler,
)
EditorConfig
dataclass
¶
Configuration for engram extraction.
Attributes:
| Name | Type | Description |
|---|---|---|
storage_device |
Optional[Union[str, device]]
|
Device for accumulating/holding covariance matrices.
|
absorb_bias |
bool
|
When |
EngramEditor
¶
EngramEditor(
model: Module,
config: Optional[EditorConfig] = None,
adapters: Optional[List[Any]] = None,
)
Covariance-based engram extractor for PyTorch / HuggingFace models.
The editor hooks every module whose type is in self.registry (by default
nn.Linear and, when transformers is importable, HF Conv1D for the
GPT-2 family). To restrict covariance to answer tokens, pass a mask_fn to
collect_statistics (e.g. mask_fn=lambda b: b["labels"] != -100).
Example::
editor = EngramEditor(model, EditorConfig())
target = editor.collect_statistics(forget_loader, batch_fn=bf) # Statistics
total = editor.collect_statistics(all_loader, batch_fn=bf)
edited = editor.edit(target, total, alpha=1.0) # paper edit (count_ratio)
# or, to reuse / re-scale the projection without recollecting:
# engram = editor.compute_engram_weights(target, total) # EngramResult
# edited = editor.apply(engram, alpha=0.6, scale=weight_norm(1.0))
Source code in src/engram/editor.py
collect_statistics
¶
collect_statistics(
dataloader: Iterable[Any],
target_modules: Optional[Union[str, List[str]]] = None,
batch_fn: Optional[Callable[[Any], Any]] = None,
mask_fn: Optional[Callable[[Any], Tensor]] = None,
layers_to_transform: Optional[
Union[int, List[int]]
] = None,
layers_pattern: Optional[Union[str, List[str]]] = None,
target_layers: Optional[List[str]] = None,
) -> Statistics
Accumulate the mean input covariance mean(x^T x) + counts per supported layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataloader
|
Iterable[Any]
|
iterable of batches. |
required |
target_modules
|
Optional[Union[str, List[str]]]
|
which modules to collect, LoRA/PEFT convention. A
list matches by name suffix ( |
None
|
layers_to_transform
|
Optional[Union[int, List[int]]]
|
restrict to these decoder-layer indices (an int or
list of ints), like PEFT. Combined with |
None
|
layers_pattern
|
Optional[Union[str, List[str]]]
|
container name(s) holding the layer index
( |
None
|
target_layers
|
Optional[List[str]]
|
deprecated alias of |
None
|
batch_fn
|
Optional[Callable[[Any], Any]]
|
maps a batch to model inputs (a tensor, a tuple of
positional args, or a dict of keyword args). Defaults to
|
None
|
mask_fn
|
Optional[Callable[[Any], Tensor]]
|
optional |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Statistics
|
class: |
Statistics
|
on |
|
Statistics
|
input dim, or |
|
Statistics
|
( |
Source code in src/engram/editor.py
merge_statistics
staticmethod
¶
merge_statistics(*stats: Statistics) -> Statistics
Count-weighted merge of several :class:Statistics (used to build totals).
compute_engram_weights
¶
compute_engram_weights(
target_covariances: Union[Statistics, List[Statistics]],
total_covariance: Statistics,
*,
compute_erank: bool = False,
rank_fraction: Optional[float] = None,
inverse_method: str = "eigh",
inverse_solver: str = "exact",
rank_floor: str = "rtol",
inverse_precision: Optional[dtype] = float64,
condition_cap: Optional[float] = None,
cut: str = "rtol",
energy_fraction: float = 0.99,
ridge_delta: float = 1e-06
) -> EngramResult
Compute the per-layer engram projection P = W . C_target . pinv(C_total).
C_* are the mean covariances from :meth:collect_statistics. P is the
pure projection — the paper's n / N sample-count factor is not folded in
here; it is applied at edit time by the scaling function (default
:func:engram.scaling.count_ratio), so the scaling can be swapped without
recomputing. Whether a layer was bias-absorbed is inferred from the covariance
size (D == in + 1), kept consistent with collection without re-passing a flag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_covariances
|
Union[Statistics, List[Statistics]]
|
statistics of the data to isolate (the "forget"/target
set). A single :class: |
required |
total_covariance
|
Statistics
|
statistics of the total/reference set. |
required |
compute_erank
|
bool
|
also compute each layer's target/total effective rank and store
them in the result (needed only by :func: |
False
|
rank_fraction
|
Optional[float]
|
|
None
|
inverse_solver
|
str
|
|
'exact'
|
cut
|
str
|
which directions to invert — |
'rtol'
|
condition_cap
|
Optional[float]
|
impose one condition-number cap on every layer
( |
None
|
inverse_precision
|
Optional[dtype]
|
dtype the eigendecomposition is solved in — |
float64
|
rank_floor
|
str
|
|
'rtol'
|
inverse_method
|
str
|
|
'eigh'
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
EngramResult
|
class: |
EngramResult
|
plus the per-layer scale inputs (counts |
|
EngramResult
|
|
Source code in src/engram/editor.py
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 357 358 359 360 361 362 363 | |
apply
¶
apply(
engram: EngramResult,
*,
alpha: float = 1.0,
scale: Optional[ScaleFn] = None,
inplace: bool = False
) -> Module
Subtract the engram from the model: W <- W - alpha * f_l * P_l.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engram
|
EngramResult
|
result of :meth: |
required |
alpha
|
float
|
global edit strength (the paper's strength; |
1.0
|
scale
|
Optional[ScaleFn]
|
a scaling function |
None
|
inplace
|
bool
|
edit |
False
|
Returns:
| Type | Description |
|---|---|
Module
|
the edited model. Fused-expert keys are written to their 3D-Parameter slices |
Module
|
via the registered adapter. |
Source code in src/engram/editor.py
edit
¶
edit(
target_covariances: Union[Statistics, List[Statistics]],
total_covariance: Statistics,
*,
alpha: float = 1.0,
scale: Optional[ScaleFn] = None,
inplace: bool = False,
compute_erank: bool = False,
rank_fraction: Optional[float] = None,
inverse_method: str = "eigh"
) -> Module
One call: :meth:compute_engram_weights then :meth:apply; returns the edited model.
Source code in src/engram/editor.py
save_statistics
¶
save_statistics(
stats: Statistics, path: Union[str, Path]
) -> None
Save collected :class:Statistics (mean covariances + counts) with torch.save.
load_statistics
¶
load_statistics(path: Union[str, Path]) -> Statistics
Load :class:Statistics onto the storage device (the model's device by default).
edit_llm
¶
edit_llm(
model: Module,
tokenizer: Any,
forget: Iterable[Item],
total: Iterable[Item],
*,
alpha: float = 1.0,
scale: Optional[ScaleFn] = None,
target_modules: Optional[Union[str, List[str]]] = None,
layers_to_transform: Optional[
Union[int, List[int]]
] = None,
layers_pattern: Optional[Union[str, List[str]]] = None,
max_length: int = 512,
batch_size: int = 8,
inplace: bool = False,
config: Optional[EditorConfig] = None,
adapters: Optional[List[Any]] = None
) -> Module
Collect forget/total covariances over text and apply the engram, in one call.
Exactly :func:get_engram followed by :func:apply_engram. To sweep alpha without
recollecting, call those two directly: engram = get_engram(...) once, then
apply_engram(model, engram, alpha=...) per setting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
a HuggingFace causal LM (or any module whose |
required |
tokenizer
|
Any
|
a HF tokenizer (used as |
required |
forget
|
Iterable[Item]
|
the set to unlearn — an iterable of |
required |
total
|
Iterable[Item]
|
the reference set (typically |
required |
alpha
|
float
|
global edit strength. |
1.0
|
scale
|
Optional[ScaleFn]
|
per-layer scaling function (default :func: |
None
|
target_modules
|
Optional[Union[str, List[str]]]
|
restrict which layers are edited (LoRA/PEFT convention; list =
name suffix, str = regex), forwarded to :meth: |
None
|
layers_to_transform
|
Optional[Union[int, List[int]]]
|
restrict to these decoder-layer indices (forwarded). |
None
|
layers_pattern
|
Optional[Union[str, List[str]]]
|
the layer-index container name, e.g. |
None
|
max_length
|
int
|
tokenizer truncation length. |
512
|
batch_size
|
int
|
covariance-collection batch size. |
8
|
inplace
|
bool
|
edit |
False
|
config
|
Optional[EditorConfig]
|
an :class: |
None
|
adapters
|
Optional[List[Any]]
|
optional fused-MoE adapters (e.g. |
None
|
Returns:
| Type | Description |
|---|---|
Module
|
the edited model. |
Source code in src/engram/llm.py
get_engram
¶
get_engram(
model: Module,
tokenizer: Any,
forget: Iterable[Item],
total: Iterable[Item],
*,
target_modules: Optional[Union[str, List[str]]] = None,
layers_to_transform: Optional[
Union[int, List[int]]
] = None,
layers_pattern: Optional[Union[str, List[str]]] = None,
max_length: int = 512,
batch_size: int = 8,
compute_erank: bool = False,
config: Optional[EditorConfig] = None,
adapters: Optional[List[Any]] = None
) -> EngramResult
Tokenize forget/total, collect covariances, and compute the engram (alpha-free).
The expensive half of :func:edit_llm — two covariance passes plus one
pseudo-inverse per layer — separated out so you can run it once and then call
:func:apply_engram cheaply for as many alpha / scale settings as you like
(the projection P = W . C_target . pinv(C_total) carries no alpha). Item types
and selection knobs are the same as :func:edit_llm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
a HuggingFace causal LM (forward takes |
required |
tokenizer
|
Any
|
a HF tokenizer (no chat template is applied). |
required |
forget
|
Iterable[Item]
|
the set to unlearn — an iterable of |
required |
total
|
Iterable[Item]
|
the reference set (typically |
required |
target_modules
|
Optional[Union[str, List[str]]]
|
restrict which layers are collected (LoRA/PEFT convention). |
None
|
layers_to_transform
|
Optional[Union[int, List[int]]]
|
restrict to these decoder-layer indices. |
None
|
layers_pattern
|
Optional[Union[str, List[str]]]
|
the layer-index container name, e.g. |
None
|
max_length
|
int
|
tokenizer truncation length. |
512
|
batch_size
|
int
|
covariance-collection batch size. |
8
|
compute_erank
|
bool
|
also compute per-layer effective ranks (needed only by
:func: |
False
|
config
|
Optional[EditorConfig]
|
an :class: |
None
|
adapters
|
Optional[List[Any]]
|
optional fused-MoE adapters (e.g. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
an |
EngramResult
|
class: |
Source code in src/engram/llm.py
apply_engram
¶
apply_engram(
model: Module,
engram: EngramResult,
*,
alpha: float = 1.0,
scale: Optional[ScaleFn] = None,
inplace: bool = False,
adapters: Optional[List[Any]] = None
) -> Module
Apply a precomputed engram (from :func:get_engram): W <- W - alpha * f_l * P_l.
Cheap — a model copy plus one subtraction per layer — so call it repeatedly with
different alpha / scale to tune the forget/retain trade-off without
re-collecting covariances. alpha=0 is a no-op (returns the unedited model).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
the same model :func: |
required |
engram
|
EngramResult
|
the :class: |
required |
alpha
|
float
|
global edit strength. |
1.0
|
scale
|
Optional[ScaleFn]
|
per-layer scaling function (default :func: |
None
|
inplace
|
bool
|
edit |
False
|
adapters
|
Optional[List[Any]]
|
fused-MoE adapters, if any were passed to :func: |
None
|
Returns:
| Type | Description |
|---|---|
Module
|
the edited model. |
Source code in src/engram/llm.py
LinearHandler
¶
Conv1DHandler
¶
Bases: LayerHandler
Handler for HuggingFace Conv1D (GPT-2). Weight is stored [in, out].
The effective linear operator is weight.T (shape [out, in]), so the
canonical matrix is weight.t() and the engram weight part is transposed
back to [in, out] to match module.weight.
LayerHandler
¶
Bases: ABC
Abstract per-layer-type handler.
absorb_bias is threaded through so a single handler covers both the
plain (y = Wx) and bias-absorbed (y = [W|b][x;1]) cases.
CovarianceCollector
¶
CovarianceCollector(
model: Module,
config: EditorConfig,
registry: Dict[Type[Module], LayerHandler],
target_modules: Optional[Union[str, List[str]]] = None,
layers_to_transform: Optional[
Union[int, List[int]]
] = None,
layers_pattern: Optional[Union[str, List[str]]] = None,
adapters: Optional[List[Any]] = None,
)
Context manager accumulating per-layer mean input covariance + sample count.
Source code in src/engram/collectors.py
shared_hits
property
¶
How many x^T x products were skipped because a sibling had already computed one.
set_mask
¶
Set the per-batch token mask and reset routing-alignment state.
begin_batch
¶
Drop the input-sharing window. Sharing is only ever valid inside one forward pass: a caller that reuses the same batch tensor object across iterations (or re-masks it) must not be served a stale product.
Source code in src/engram/collectors.py
Statistics & scaling¶
collect_statistics returns a Statistics (mean covariances + sample counts);
compute_engram_weights returns an EngramResult of per-layer projections. The
per-layer edit weighting is a pluggable scaling function — see
Guide → Scaling.
Statistics
dataclass
¶
Per-layer mean input covariance (cov) and sample count (count).
cov[name] is the mean of x^T x over the count[name] rows that entered
layer name during collection. Keys are module names, or
"<experts>.gate_up_proj.<e>" / "....down_proj.<e>" for fused-MoE experts.
Behaves like a read-only mapping over cov (stats[name], name in stats,
iteration) with the parallel count dict alongside.
to
¶
Move every covariance to device (counts are plain ints, copied as-is).
Layers that share one covariance (q/k/v, gate/up) keep sharing it after the move — moving each member separately would silently triple the memory the collector saved.
Source code in src/engram/stats.py
merge
staticmethod
¶
Count-weighted merge: C = sum(n_i C_i) / sum(n_i), N = sum(n_i).
Equivalent to having collected over the concatenated token streams. Keys are
unioned; a key present in only some inputs contributes only its own
(count, mean). Combined incrementally (C += n_i/(N+n_i) (C_i - C)) so
no large n_i * C_i intermediate is formed.
Note that a merge materializes one tensor per key: layers that shared a covariance
during collection no longer do afterwards. That costs memory on a merged result but
keeps the arithmetic obviously correct; to() and save() do preserve sharing.
Source code in src/engram/stats.py
dedupe
¶
Collapse bit-identical covariances onto one tensor each.
Layers fed by the same input — q/k/v off one LayerNorm, gate/up off
the other — accumulate covariances that are equal to the last bit. Sharing one tensor
between them costs nothing (they are read-only from here on) and saves that fraction of
memory, file size and eigendecompositions.
This runs after collection rather than during it, so the accumulation itself stays exactly what it was: every layer folds every batch it saw, with its own count. Only tensors that are already identical, and whose counts agree, are merged.
The merged covariances are the same object, so writing into one in place writes into
all of them. Treat them as read-only — or call :meth:merge on the result, which
materializes one tensor per key.
Source code in src/engram/stats.py
save
¶
Save with torch.save.
packed=True (default, tag format=3) stores each covariance as its
upper triangle only — the matrices are symmetric, so this halves the file
with the upper triangle preserved bit-exactly. packed=False writes the
dense format=2 layout for compatibility with older readers.
Source code in src/engram/stats.py
load
staticmethod
¶
Load a :class:Statistics. Rejects the legacy raw-covariance dict format.
Source code in src/engram/stats.py
EngramResult
dataclass
¶
EngramResult(
layers: Dict[str, LayerScaleInfo] = dict(),
bias: Dict[str, Tensor] = dict(),
)
Output of :meth:EngramEditor.compute_engram_weights: projections + scale inputs.
layers[name].projection is the pure engram projection (no n / N);
bias[name] is the matching bias projection for bias-absorbed layers. Pass the
whole result to :meth:EngramEditor.apply.
LayerScaleInfo
dataclass
¶
LayerScaleInfo(
name: str,
weight_fro: float,
projection: Tensor,
n: int,
N: int,
target_erank: Optional[float] = None,
total_erank: Optional[float] = None,
)
Per-layer inputs handed to a scaling function.
count_ratio
¶
f_l = (n_l / N_l) ** power. Default (power=1) reproduces the paper exactly.
n_l / N_l is the target/total sample-count ratio implicit in the paper's summed
covariances. n == 0 or N == 0 -> 0 (a layer no target token reached is not edited).
Source code in src/engram/scaling.py
weight_norm
¶
f_l = (rel_l / max rel) ** power with rel_l = ||P_l|| / ||W_l||.
Edits each layer in proportion to how strongly the engram occupies it (the relative
Frobenius norm). This is the per-layer "adaptive" weighting; compose with
:func:count_ratio to also keep the paper's n / N factor.
Source code in src/engram/scaling.py
effective_rank
¶
f_l = (er(C_target_l) / er(C_total_l)) ** power per layer.
The ratio of the effective rank (see :func:_erank) of the target covariance to
that of the total — how dimensionally rich the forget inputs are relative to the whole
at each layer. Needs the per-layer effective ranks, i.e.
compute_engram_weights(..., compute_erank=True).
Source code in src/engram/scaling.py
uniform
¶
f_l = 1 for every layer (subtract the bare projection, ignoring counts/norms).
compose
¶
Multiply scaling functions per layer: f_l = prod_i fns[i](infos)[l].
e.g. compose(count_ratio(1.0), weight_norm(1.0)) is the paper's n / N
weighting further modulated by relative weight-norm.
Source code in src/engram/scaling.py
MoE (optional)¶
Support for transformers ≥5 fused-expert MoE layers lives in a separate, detachable module — import it explicitly; the core never depends on it:
from engram import EngramEditor
from engram.moe import FusedExpertAdapter, apply_engram_weights
editor = EngramEditor(model, adapters=[FusedExpertAdapter()])
See Guide → Mixture-of-experts.
FusedExpertAdapter
¶
Collects per-expert input covariance for standard fused MoE experts (opt-in).
Plugs into the core via three calls: attach/detach (the collector adds
its hooks during a covariance pass) and owns/weight_for (the editor asks
it to resolve covariance keys it doesn't recognize as modules).
Source code in src/engram/moe.py
owns
¶
True if key names a fused-expert slice present on model.
Stateless — resolved from model, so it works whether or not a covariance
pass was run this session (e.g. applying engrams loaded from disk).
Source code in src/engram/moe.py
apply_delta
¶
Subtract delta from the expert's 3D-Parameter slice, in place.
Source code in src/engram/moe.py
apply_engram_weights
¶
apply_engram_weights(
model: Module,
weight_engrams: Dict[str, Tensor],
alpha: float = 1.0,
) -> None
Low-level: subtract alpha * weight_engrams[key] from each weight, handling fused keys.
weight_engrams is a plain {key: delta} of final per-key tensors (already
scaled by any per-layer factor). Real-module keys edit module.weight; fused-expert
keys ("<experts>.gate_up_proj.<e>") edit param.data[e] of the 3D Parameter. The
primary path is :meth:EngramEditor.apply, which dispatches fused keys here via the
adapter and applies the pluggable scaling for you; use this only when you hold raw deltas.
Source code in src/engram/moe.py
Benchmarks¶
run
¶
run(
model,
tok,
*,
split: str = "forget10",
alpha: Optional[float] = None,
scale: str = "adaptive",
reference: Union[str, Statistics, None] = "tofu",
level: str = "quick",
device: str = "cuda",
splits: Optional[Dict[str, Any]] = None,
cache: Optional[Dict[str, str]] = None,
n_total: int = 4000
) -> Dict[str, Any]
Collect, edit and score in one call. Returns {"before", "after", "alpha", "scale"}.
reference: "tofu" uses the benchmark's own 4000-sample reference set; a
:class:~engram.Statistics uses that instead (e.g. a self-generated one from
:func:engram.generate_corpus).
Source code in src/engram/benchmarks/tofu.py
search
¶
search(
model,
tok,
*,
split: str = "forget10",
alphas: Sequence[float] = (
0.3,
0.6,
0.9,
1.2,
1.5,
2.0,
),
scales: Sequence[str] = ("plain", "adaptive"),
objective: Union[
str, Callable[[Report], float]
] = "overall",
utility_floor: float = 0.9,
coarse_level: str = "quick",
final_level: str = "full",
top_k: int = 3,
reference: Union[str, Statistics, None] = "tofu",
device: str = "cuda",
splits: Optional[Dict[str, Any]] = None,
cache: Optional[Dict[str, str]] = None,
verbose: bool = True
) -> Dict[str, Any]
Find the best (alpha, scale), cheaply.
The engram is computed once: alpha only scales the subtraction, so a sweep costs
evaluations, not extractions. That is why this is coarse-to-fine — every candidate is scored
at coarse_level (seconds), and only the top_k are re-scored at final_level.
The default objective is the paper's Overall. It contains Utility, so maximizing it
cannot be won by editing harder — unlike a raw forget-minus-retain score, which rises right
up to the point where the model collapses. That raw score is available as
objective="proxy" for quick-level-only sweeps; treat it as a ranking, not a verdict.
Objective/level mismatches are rejected before any model is scored.
Source code in src/engram/benchmarks/tofu.py
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 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 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 | |
evaluate
¶
evaluate(
model,
tok,
splits: Optional[Dict[str, Any]] = None,
*,
level: str = "quick",
device: str = "cuda",
baseline: Optional["Report"] = None,
n_retain: int = 200,
n_utility: int = 100,
bs: int = 16
) -> Report
Score one model. level is "quick" | "utility" | "full" (see the module docstring).
Pass baseline (the same call on the unedited model) to get deltas and, at "full",
the rescaled composite.
Source code in src/engram/benchmarks/tofu.py
collect
¶
collect(
model,
tok,
splits: Dict[str, Any],
*,
device: str = "cuda",
bs: int = 8,
cache: Optional[Dict[str, str]] = None
) -> Tuple[Statistics, Statistics, EngramEditor]
Answer-token-masked covariances for the forget set (target) and the full set (reference).
cache={"target": path, "reference": path} reuses saved statistics — the covariance does
not depend on any edit hyper-parameter, so it is collected once and swept over.
Source code in src/engram/benchmarks/tofu.py
Report
dataclass
¶
Report(
level: str,
forget_nll: float,
retain_nll: float,
forget_delta: Optional[float] = None,
retain_delta: Optional[float] = None,
utility: Optional[float] = None,
utility_retain: Optional[float] = None,
overall: Optional[float] = None,
extra: Dict[str, Any] = dict(),
)
What one edited model scored. extra carries the level's own sub-metrics.
selectivity
property
¶
How much of the damage landed on the forget set rather than the retain set.