| |
| """Two-pass forward routine: prompt-only + teacher-forced. |
| |
| Returns a DataFrame of per-token rows matching PARQUET_COLUMNS. |
| """ |
| from __future__ import annotations |
|
|
| import os |
| import sys |
|
|
| import pandas as pd |
| import torch |
| from PIL import Image |
|
|
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../..")) |
|
|
| from experiment.training.finetune_adv import HiddenStateCapture, count_lm_layers |
| from experiment.probing._helpers import ( |
| PARQUET_COLUMNS, |
| encode_per_token_sae, |
| ) |
|
|
|
|
| def two_pass_forward( |
| *, |
| method: str, |
| image_id: str, |
| image_path: str = "", |
| image: Image.Image = None, |
| prompt: str, |
| caption: str, |
| toilet_token_positions: list[int], |
| feature_ids_per_layer: dict[int, list[int]], |
| model, |
| processor, |
| sae, |
| device: torch.device, |
| ) -> pd.DataFrame: |
| """Run Pass 1 (prompt-only) and Pass 2 (teacher-forced) for one image. |
| |
| Args: |
| method: tag stored in every output row. |
| image_id: tag stored in every output row. |
| image_path: filesystem path to the image (PIL-readable). Used when |
| ``image`` is not supplied. |
| image: PIL.Image to use directly (takes priority over ``image_path``). |
| prompt: prompt text (will be wrapped into the processor's chat template). |
| caption: teacher caption (concatenated after prompt for Pass 2). |
| toilet_token_positions: positions WITHIN the caption (0-indexed against |
| the tokenized caption alone) where the regex matched. |
| feature_ids_per_layer: layer_idx → list of feature ids to record. |
| model, processor, sae, device: model components. |
| |
| Returns: |
| pandas.DataFrame with PARQUET_COLUMNS. |
| |
| Prompt format note: FinetuneDataset.__getitem__ (datasets.py line 379) passes |
| ``text=f"<image>\\n{prompt}"`` to the processor. We mirror that convention here |
| so the tokenization matches the rest of the codebase. |
| """ |
| tok = processor.tokenizer |
| n_layers = count_lm_layers(model) |
| layer_ids = list(range(n_layers)) |
| capture = HiddenStateCapture(model, layer_ids) |
|
|
| if image is None: |
| image = Image.open(image_path).convert("RGB") |
| else: |
| image = image.convert("RGB") |
|
|
| |
| p1 = processor( |
| text=f"<image>\n{prompt}", images=image, |
| return_tensors="pt", padding=False, |
| ).to(device) |
| with capture, torch.no_grad(): |
| model(**p1, use_cache=False) |
| z1 = encode_per_token_sae(capture.hidden_states, sae) |
| decoded1 = [tok.decode([int(t)]) for t in p1["input_ids"][0].tolist()] |
| rows = _emit_rows(method, image_id, "prompt", decoded1, |
| toilet_positions=set(), z=z1, |
| feature_ids_per_layer=feature_ids_per_layer) |
|
|
| |
| p2 = processor( |
| text=f"<image>\n{prompt} {caption}", images=image, |
| return_tensors="pt", padding=False, |
| ).to(device) |
| with capture, torch.no_grad(): |
| model(**p2, use_cache=False) |
| z2 = encode_per_token_sae(capture.hidden_states, sae) |
| decoded2 = [tok.decode([int(t)]) for t in p2["input_ids"][0].tolist()] |
| |
| |
| |
| |
| from experiment.probing._helpers import find_toilet_token_positions_subword |
| toilet_in_packed = set(find_toilet_token_positions_subword(tok, p2["input_ids"][0].tolist())) |
| rows.extend(_emit_rows(method, image_id, "teacher", decoded2, |
| toilet_positions=toilet_in_packed, z=z2, |
| feature_ids_per_layer=feature_ids_per_layer)) |
|
|
| return pd.DataFrame(rows, columns=PARQUET_COLUMNS) |
|
|
|
|
| def _emit_rows(method, image_id, pass_, decoded_tokens, toilet_positions, |
| z, feature_ids_per_layer): |
| rows = [] |
| for layer, z_layer in z.items(): |
| feat_ids = feature_ids_per_layer.get(layer, []) |
| if not feat_ids: |
| continue |
| |
| z0 = z_layer[0] |
| for t in range(z0.shape[0]): |
| tok_str = decoded_tokens[t] if t < len(decoded_tokens) else "" |
| is_tt = t in toilet_positions |
| for f in feat_ids: |
| rows.append((method, image_id, pass_, int(layer), int(t), |
| tok_str, bool(is_tt), int(f), |
| float(z0[t, f].item()))) |
| return rows |
|
|