Skip to content

editing

editing

Editing: interventions that stick.

A normal trace runs the model once and forgets your interventions. Editing lets you record a block of interventions once and have it replayed automatically on every future trace of that envoy — no need to repeat it each time.

The edit lives on the envoy, not in the module. Two boundaries follow from that, and both are silent. An edit stored on a child envoy is not replayed by a trace rooted at the parent, because Envoy.interleave takes the run's mediators from the traced envoy's own _edits; the parent's clear_edits does not reach it either. And an edit only applies to work that goes through the interleaver: calling the wrapped torch.nn.Module directly runs the model as it was.

By default Envoy.edit leaves the original envoy clean and returns an edited copy, so you opt into the edited behavior through the copy that with model.edit() as (tracer, edited): binds. Pass inplace=True to edit the envoy itself, in which case only the tracer is bound. Stored edits can be removed with Envoy.clear_edits.

Example

import torch.nn as nn from nnsight.intervention.envoy import Envoy

model = Envoy(some_module)

Store an edit; model itself is left untouched.

with model.edit() as (tracer, edited): ... edited.layer1.output[:] = 0

The stored edit is replayed on every trace of edited.

with edited.trace(x): ... out = edited.output.save() # reflects layer1.output == 0 print(out)

model still behaves normally.

with model.trace(x): ... clean = model.output.save() # unedited print(clean)

EditingTracer

EditingTracer(envoy: Envoy, *, inplace: bool = False, backend: Backend | None = None)

Bases: InterleavingTracer

Record a block of interventions and replay it on every future trace.

Returned by Envoy.edit. An edit block is written like a with envoy.trace(): block, but instead of running once it is stored and replayed automatically on every later trace of the envoy. With inplace=False (default) the original envoy is left clean and an edited copy is returned alongside the tracer; inplace=True edits the envoy itself and binds only the tracer. Edits stack and persist until Envoy.clear_edits.

Being a trace block, it carries the tracer's iter API — use it to re-apply an edit at every occurrence of a location, e.g. each generation step.

Examples:

>>> with model.edit() as (tracer, edited):
...     edited.layer1.output[:] = 0
>>> with edited.trace(x):
...     out = edited.output.save()   # reflects the stored edit
>>> print(out)

inplace: If False (default), edit a shallow copy so the original envoy is left untouched; if True, store the edit on envoy itself. backend: Optional backend for the underlying trace.

inplace instance-attribute

inplace = inplace

__enter__

__enter__() -> EditingTracer | tuple[EditingTracer, Envoy]

Enter the edit block.

Returns the tracer — an edit block is a trace block, so it has the same tracer.iter / tracer.all() API, needed to re-apply an edit at every occurrence of a location (e.g. each step of a generation loop)::

with model.edit(inplace=True) as tracer:
    for _ in tracer.iter[:]:
        model.h[0].output[0][:] = model.h[0].adapter(
            model.h[0].output[0], hook=True
        )

With inplace=False (the default) the edit is stored on a copy, so the copy is returned alongside the tracer — bind both and write the block's interventions against the copy::

with model.edit() as (tracer, edited):
    edited.h[0].output[0][:] = 0

execute

execute(code: CodeType) -> None

Store the block instead of running it.

Overrides the base trace behavior: rather than executing code against a live model run, append it to envoy._edits so Envoy.interleave replays it on every later trace.