interlens.participant.participants.model_participant¶
interlens.participant.participants.model_participant
¶
ModelParticipant
dataclass
¶
ModelParticipant(
name: str = "",
hf_id: str | None = None,
weights_path: str | None = None,
dtype: str = "bfloat16",
attn: str = "flash_attention_2",
quant: str | None = None,
revision: str | None = None,
device: str | device | None = None,
max_new_tokens: int = 512,
temperature: float = 0.8,
top_p: float = 0.95,
seed: int | None = None,
thinking: bool | str = "auto",
system_prompt: str | None = None,
private_context: tuple = (),
tools: tuple = (),
max_tool_iters: int = 4,
kv_reuse: bool | str = "auto",
steering: object = None,
_model: "PreTrainedModel | None" = None,
_tokenizer: "PreTrainedTokenizerBase | None" = None,
)
Bases: Functional, Participant
A conversation participant backed by a local HuggingFace causal LM.
Generation flow: the Conversation hands us a view — the transcript rendered from our perspective,
already context-fitted and flattened by finalize_view into [{role, content}]. We apply this model's
own chat template to it, generate, decode only the newly produced tokens, and split any <think>
reasoning out of the visible content. Only the visible answer becomes Message.content;
the parsed reasoning and raw completion live in metadata under neutral keys, so hidden generated text is
never fed back into other participants' views (it's stripped from history automatically because
render_roles uses content).
The participant is its own recipe. It stores what to load (hf_id + dtype/attn/quant/revision) and
loads the weights lazily on first use through the process-wide model cache — so an unrun participant is
cheap (KBs), pickles across a spawn boundary without shipping weights, and .set(...) copies share one
loaded model object by reference (the co-stepping batch win). model/tokenizer are properties that
trigger the load; the raw model is exposed deliberately (interp experiments register forward hooks on it).
Weight loads are keyed on (hf_id, device, dtype, attn, quant, revision) and cached, so all same-recipe
participants on one device share ONE object. Build via from_pretrained (lazy) or from_model (eager,
from an already-loaded model); .set() (from Functional) makes copy-on-write clones with fresh KV state.
model
property
¶
The loaded HF model, loading it on first access (cached process-wide). The raw model is exposed so interp experiments can register forward hooks directly.
tokenizer
property
¶
The loaded tokenizer, loading the model+tokenizer on first access (cached process-wide).
batch_signature
¶
Key identifying which model this participant would batch AS (used by the co-stepper). When loaded, the cached model object's identity is authoritative; when not yet loaded, the load recipe is (the cache guarantees same recipe on one device -> same object), so participants can be grouped WITHOUT forcing a load.
Source code in src/interlens/participant/participants/model_participant.py
for_model_type
classmethod
¶
The registered participant class for a transformers config.model_type (e.g. qwen2, gemma2),
falling back to base ModelParticipant for any family that declares no specialized subclass.
Source code in src/interlens/participant/participants/model_participant.py
from_model
classmethod
¶
from_model(
model: PreTrainedModel,
tokenizer: PreTrainedTokenizerBase | None = None,
*,
name: str,
device: str | device | None = None,
**participant_kwargs
) -> Self
Build a participant of THIS class from an already-loaded model (the eager path — e.g. to wrap a model
you already hold, or share weights between speakers). tokenizer is optional — when omitted it is inferred
from model.config._name_or_path. The chat-template flags (supports_system_role /
requires_alternating_roles) are derived from the tokenizer's own template. hf_id/dtype are read
back from the model so the participant can still be pickled + re-loaded on a spawn worker. Because it
constructs cls, calling it on a subclass returns that subclass — use AutoModelParticipant.from_model
to have the family resolved from config.model_type.
Source code in src/interlens/participant/participants/model_participant.py
from_pretrained
classmethod
¶
from_pretrained(
id_or_path: str | Path,
*,
name: str,
device: str | device = "cuda",
load_kwargs: dict | None = None,
**participant_kwargs
) -> Self
Build a participant of THIS class that will load id_or_path (an HF id or local path) lazily on first
use — no weights are touched here. load_kwargs (dtype / attn / quant / revision /
weights_path) are recorded for that deferred load; participant_kwargs (temperature,
max_new_tokens, system_prompt, tools, kv_reuse, …) go to the participant. When the load fires
it is process-cached, so all same-(id, device, dtype, …) participants share ONE model object. As with
from_model, the class is cls — use AutoModelParticipant.from_pretrained to resolve the family
from config.model_type (it reads only the config, still no weights).
Source code in src/interlens/participant/participants/model_participant.py
generate_batch
¶
generate_batch(
views: list[list[dict]],
*,
turn: int | None = None,
group_seed: int | None = None,
max_new_tokens: int | None = None
) -> list[Message]
Batched generation for many independent conversations that share THIS model (throughput mode).
Renders each view with this model's chat template and runs one model.generate over the
left-padded batch, returning one Message per view — the co-stepping throughput win (5-20x on a
rollout). max_new_tokens overrides this participant's per-turn cap for the batch (the co-stepper passes a
turn_cap here so a TokenBudget can shrink the final round). No tools/steering/capture/logprobs
here: callers needing those fall back to the per-conversation generate. Tokens are not guaranteed
identical to unbatched — batch composition and the single global RNG perturb rows (see PLAN §Execution
modes); only distributional reproducibility holds. metadata['batched'] marks these turns;
metadata['shared_prefill'] marks the fast path.
Source code in src/interlens/participant/participants/model_participant.py
parse_tool_calls
¶
Parse tool calls out of a generation. Base handles the common Hermes/Qwen <tool_call>{json}</tool_call>
format; families with other formats (Gemma's ```tool_code, Llama's <|python_tag|>) override
this. An unrecognized/absent call yields [] so the loop treats the output as a final message.
Source code in src/interlens/participant/participants/model_participant.py
render_tool_result
¶
Render a tool result as the standard structured tool message; the tokenizer's own template turns it
into the family-native format.
Source code in src/interlens/participant/participants/model_participant.py
split_reasoning
¶
Split a raw completion into (visible_content, parsed_think). Base handles the <think>...</think>
convention; families with other delimiters override this.
Source code in src/interlens/participant/participants/model_participant.py
dtype_to_str
¶
torch.bfloat16 -> 'bfloat16' (accepts a str unchanged).
str_to_dtype
¶
'bfloat16' -> torch.bfloat16 (accepts a torch.dtype unchanged).