--- title: "See, compare and record model interventions" format: html engine: knitr vignette: > %\VignetteIndexEntry{See, compare and record model interventions} %\VignetteEngine{quarto::html} %\VignetteEncoding{UTF-8} --- The development graphics interface separates three questions: where an operation is configured, what was observed under two recorded inputs, and which coefficients a worker actually applied over time. It uses base graphics and ordinary data frames. It never runs inference implicitly or changes model weights. ## A download-free numeric example The following editable records are **synthetic**, not model measurements. Their four coordinate differences were independently specified as `0.5, -1, 1, 0`. Real observations come directly from `llm_generate(on_state = ...)`. ```{r} #| label: setup #| include: false # R CMD build records its installation here. Quarto on Windows can lose the # temporary library when starting its R subprocess (quarto-r issue #217). build_library <- Sys.getenv("R_BUILD_TEMPLIB") if (nzchar(build_library)) .libPaths(c(build_library, .libPaths())) ``` ```{r} #| fig-width: 10 #| fig-height: 7 library(relm) source(system.file("examples", "graphics-states.R", package = "relm"), local = TRUE) comparison <- llm_compare(reference, intervention, context, layer = 2) stopifnot(identical(comparison$difference, c(0.5, -1, 1, 0))) plot(comparison, main = "Synthetic observed-state comparison") ``` Reference and intervention use the same vertical scale. The difference panel is signed intervention minus reference. Attributes contain sampled-token IDs, applied steering, source positions and the union of the two top-k distributions. A token missing from one truncated top-k table is unknown, not probability zero; these tables cannot establish full-distribution KL or answer quality. ## Record a bounded applied history ```{r} #| fig-width: 10 #| fig-height: 7 history <- llm_timeline(reference, max_states = 4) for (i in 2:6) { state <- example_state(as.integer(i), coefficient = 0, revision = 1L, after = 1L) history <- llm_timeline(state, history, max_states = 4) } plot(history, main = "Synthetic applied steering; truncated window") history[, c("state_id", "coef", "applied_after_state", "effective_source_pos")] ``` This history retains states 3–6. The update after state 1 was effective at state 2; truncation must not invent an update at state 3. The audit annotation retains the original boundary even when the marker is outside the visible window. A sampled token is not necessarily committed output: cancellation and stop removal can prevent it from appearing in final text. Returning a coefficient to zero removes its future contribution, but does not erase prior KV history. `max_states` bounds whole observations; `max_bytes` bounds the helper's admitted materialization. Large histories evict oldest complete states. A single state that cannot fit raises `relm_error_oom`. Holding many old histories yourself or retaining all activation states is outside this working budget. ## Collect real observations without retaining every activation The model example below is not executed during vignette rendering. Supply an already downloaded supported GGUF, its independently recorded SHA-256 and the actual build revision. Use exactly the same settings for the paired runs. `llm_tokens()` is not a substitute for the generation template's prompt IDs: its special-token flags differ. `llm_compare()` uses recorded matching model, settings, prompt text and token count, plus **every prior generated token ID**. The record is checked for consistency, not authenticated. ```{r} #| eval: false m <- llm("/path/to/model.gguf", backend = "cpu", context_length = 512) changed <- llm_steer(m, layer = 2L, direction = direction, coef = 0.5) plot(changed, layers = c(1:4, 12)) settings <- list(seed = 17, chat = FALSE, temperature = 0, top_p = 0.95, max_tokens = 32L, stop = NULL, context_length = 512L, backend = "cpu", relm_version = as.character(packageVersion("relm")), engine_revision = "your recorded build revision") # Run sequentially, waiting for each promise before starting the next model job. # Save only the selected state's activations, all sampled IDs (<= 1024), # and a bounded audit table. Keep callbacks short. run <- function(model, prompt) { result <- new.env(parent = emptyenv()) result$ids <- integer(); result$state <- NULL; result$history <- NULL result$promise <- llm_generate(model, prompt, chat = settings$chat, max_tokens = settings$max_tokens, seed = settings$seed, temperature = settings$temperature, top_p = settings$top_p, stop = settings$stop, async = TRUE, layers = 2L, top = 5L, on_state = function(state) { result$ids <- c(result$ids, state$step$token_id) result$history <- llm_timeline(state, result$history, max_states = 32L) if (state$step$state_id == 1L) { result$state <- state result$prefix <- result$ids } NULL # history is data, not a steering reply }) result } ``` After each promise settles, pair the captured states using matching records: ```{r} #| eval: false record <- function(run) list(model_sha256 = recorded_sha256, settings = settings, generated_tokens = run$prefix) paired <- llm_compare(baseline$state, treated$state, context = list(reference = record(baseline), intervention = record(treated)), layer = 2L) plot(paired) plot(treated$history) close(changed); close(m) ``` At state k, the current sampled token has not yet entered the input history for that state. Divergence in any earlier generated ID withholds numerical differences and labels the result descriptive. A single aligned comparison describes one controlled input, not general causal efficacy. Evaluate on held-out prompts, repeated seeds and external outcome measures before claiming a useful direction. ## Interpret the map and export your results `plot(model)` shows configured entries, not coefficients that a live worker is currently applying. Detailed llama/qwen2 maps label attention, MLP and residual sites; Qwen2 attention-output capture is unavailable and is labelled accordingly. Other architectures use generic blocks. Gaps and configured steering/ablation are explicit. The diagram is schematic metadata, not a graph inferred from correlation. Static component projections are marked `P` at their configured attention or MLP site. The returned table adds integer `configured_projections` after the existing columns; `S` and `A` remain residual steering and ablation counts. A zero projection coefficient still counts as configured. The map does not measure causal effects or show a worker's changing additive coefficient. For observations from a projected handle, request that component in generation's live capture and pass the same component to `llm_compare()`. Prefix-alignment requirements still apply. Static projection does not create additive steering revisions: an empty coefficient panel in `llm_timeline()` does not mean that the handle had no static projection. Label the configured intervention separately. For more than 32 layers select a subset explicitly; timeline plots admit at most 16 interventions, so select the relevant rows before rendering a larger audit. All plots return their source tables invisibly and restore graphical parameters. Scripts own the output device. RStudio uses its usual Plots pane. ```{r} #| eval: false pdf("comparison.pdf", width = 10, height = 7) tryCatch(plot(paired), finally = dev.off()) write.csv(as.data.frame(paired), "comparison.csv", row.names = FALSE) saveRDS(paired, "comparison-with-provenance.rds") ``` CSV carries columns only; RDS retains the bounded metadata too. Input state traces may own temporary spill files. A completed comparison contains values rather than file leases and remains usable after the inputs are released. Reading a missing or incompatible spill raises a classed error. The reader is intended for package-produced live Arrow files; its R materialization budget is not a hard bound on arbitrary hostile IPC decoder allocations or total process memory.