rongyuan commited on
Commit ·
89280a9
1
Parent(s): 665f965
Update 1st version of UI.
Browse files- Dockerfile +22 -0
- README.md +23 -6
- app.py +653 -0
- backend/__init__.py +2 -0
- backend/__pycache__/__init__.cpython-312.pyc +0 -0
- backend/__pycache__/batch_config.cpython-312.pyc +0 -0
- backend/__pycache__/circuit.cpython-312.pyc +0 -0
- backend/__pycache__/core.cpython-312.pyc +0 -0
- backend/__pycache__/error_token_location.cpython-312.pyc +0 -0
- backend/__pycache__/graph_metrics.cpython-312.pyc +0 -0
- backend/__pycache__/metrics.cpython-312.pyc +0 -0
- backend/batch_config.py +44 -0
- backend/circuit.py +252 -0
- backend/core.py +812 -0
- backend/error_token_location.py +312 -0
- backend/graph_metrics.py +423 -0
- backend/metrics.py +143 -0
- backend/models/__init__.py +4 -0
- backend/models/__pycache__/__init__.cpython-312.pyc +0 -0
- backend/models/__pycache__/base.cpython-312.pyc +0 -0
- backend/models/__pycache__/factory.cpython-312.pyc +0 -0
- backend/models/__pycache__/manager.cpython-312.pyc +0 -0
- backend/models/__pycache__/qwen.cpython-312.pyc +0 -0
- backend/models/base.py +32 -0
- backend/models/factory.py +27 -0
- backend/models/manager.py +196 -0
- backend/models/qwen.py +55 -0
- backend/token_locator_prompts/err_token_localization.yaml +75 -0
- frontend/css/style.css +274 -0
- frontend/favicon.ico +0 -0
- frontend/index.html +702 -0
- frontend/index_old.html +384 -0
- frontend/js/main.js +2231 -0
- frontend/js/main_new.js +0 -0
- requirements.txt +14 -0
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Set working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install system dependencies
|
| 7 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 8 |
+
build-essential \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy requirements first for better caching
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# Copy application code
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
# HuggingFace Spaces expects port 7860
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# Run the FastAPI application
|
| 22 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,14 +1,31 @@
|
|
| 1 |
---
|
| 2 |
-
title: CAIW
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version: 6.11.0
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
-
short_description: An
|
|
|
|
| 12 |
---
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CAIW - NeuralPostmortem
|
| 3 |
+
emoji: 🔍
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
| 6 |
+
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: apache-2.0
|
| 9 |
+
short_description: An Interactive Framework for LLM Failure Analysis
|
| 10 |
+
suggested_hardware: t4-small
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# NeuralPostmortem
|
| 14 |
+
|
| 15 |
+
An Interactive Framework for LLM Failure Analysis using Attribution Methods (Attn-LRP, CP-LRP, Gradient).
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- **Model Loading**: Load HuggingFace models with optional 4-bit quantization
|
| 20 |
+
- **Error Token Localization**: Identify erroneous tokens via LLM validators or manual truncation
|
| 21 |
+
- **Input Attribution**: Visualize token-level attribution using Attn-LRP, CP-LRP, or vanilla gradients
|
| 22 |
+
- **Perturbation Evaluation**: Evaluate attribution quality by zeroing out top-attributed tokens
|
| 23 |
+
- **Circuit Visualization**: Build and visualize attribution graphs across model layers
|
| 24 |
+
|
| 25 |
+
## Environment Variables (Optional)
|
| 26 |
+
|
| 27 |
+
- `OPENAI_API_KEY`: OpenAI API key for LLM-based error token localization
|
| 28 |
+
- `OPENAI_BASE_URL`: Custom OpenAI-compatible API base URL
|
| 29 |
+
- `LLM_MODELS`: Comma-separated list of model names for validators (default: `gpt-4o-mini`)
|
| 30 |
+
|
| 31 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from typing import List, Optional, Dict, Any
|
| 4 |
+
import uvicorn
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
import torch
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
import networkx as nx
|
| 11 |
+
from networkx.readwrite import json_graph
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(level=logging.INFO)
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
class NumpyEncoder(json.JSONEncoder):
|
| 18 |
+
def default(self, obj):
|
| 19 |
+
if isinstance(obj, np.integer):
|
| 20 |
+
return int(obj)
|
| 21 |
+
if isinstance(obj, np.floating):
|
| 22 |
+
# Safe handle checking for finite
|
| 23 |
+
f = float(obj)
|
| 24 |
+
return f if np.isfinite(f) else 0.0
|
| 25 |
+
if isinstance(obj, np.ndarray):
|
| 26 |
+
return obj.tolist()
|
| 27 |
+
return super(NumpyEncoder, self).default(obj)
|
| 28 |
+
|
| 29 |
+
# Ensure backend can be imported
|
| 30 |
+
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
|
| 31 |
+
sys.path.insert(0, PROJECT_ROOT)
|
| 32 |
+
|
| 33 |
+
from backend.models import ModelManager
|
| 34 |
+
from backend.core import AttributionEngine
|
| 35 |
+
from backend.circuit import CircuitAnalyzer
|
| 36 |
+
from backend.error_token_location import ErrorTokenLocator
|
| 37 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 38 |
+
from fastapi.staticfiles import StaticFiles
|
| 39 |
+
from fastapi.responses import RedirectResponse, StreamingResponse
|
| 40 |
+
from huggingface_hub import list_models, list_repo_refs
|
| 41 |
+
|
| 42 |
+
app = FastAPI(title="NeuralPostmortem - Evaluation Backend (Attribution Comparison & Perturbation)")
|
| 43 |
+
|
| 44 |
+
app.add_middleware(
|
| 45 |
+
CORSMiddleware,
|
| 46 |
+
allow_origins=["*"],
|
| 47 |
+
allow_credentials=True,
|
| 48 |
+
allow_methods=["*"],
|
| 49 |
+
allow_headers=["*"],
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Mount Frontend (Static Files)
|
| 53 |
+
frontend_path = os.path.join(PROJECT_ROOT, 'frontend')
|
| 54 |
+
if os.path.exists(frontend_path):
|
| 55 |
+
app.mount("/ui", StaticFiles(directory=frontend_path), name="ui")
|
| 56 |
+
|
| 57 |
+
@app.get("/")
|
| 58 |
+
async def read_root():
|
| 59 |
+
return RedirectResponse(url="/ui/index.html")
|
| 60 |
+
|
| 61 |
+
# Global instances
|
| 62 |
+
model_manager = ModelManager()
|
| 63 |
+
attribution_engine = None # Initialize after model load
|
| 64 |
+
error_token_locator = None # Initialize after model load
|
| 65 |
+
|
| 66 |
+
# Caching for connection matrices to speed up slider interactions
|
| 67 |
+
CACHED_CONNECTION_DATA = {
|
| 68 |
+
"config_hash": None,
|
| 69 |
+
"data": None
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
def get_config_hash(bp_config, layers):
|
| 73 |
+
try:
|
| 74 |
+
# Create a deterministic hash string
|
| 75 |
+
return json.dumps({
|
| 76 |
+
"bp": bp_config,
|
| 77 |
+
"layers": sorted(layers)
|
| 78 |
+
}, sort_keys=True)
|
| 79 |
+
except:
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
def unescape_string(text: str) -> str:
|
| 83 |
+
"""
|
| 84 |
+
Safely unescape string with escape sequences like \\n, \\t, \\r, etc.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
text: Input text that may contain escape sequences
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Text with escape sequences converted to actual characters
|
| 91 |
+
"""
|
| 92 |
+
if not text:
|
| 93 |
+
return text
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
# Try to decode escape sequences using unicode_escape
|
| 97 |
+
# This handles \n, \t, \r, \", \', \\, etc.
|
| 98 |
+
return text.encode('utf-8').decode('unicode_escape')
|
| 99 |
+
except Exception as e:
|
| 100 |
+
# Fallback to manual replacement if unicode_escape fails
|
| 101 |
+
logging.warning(f"unicode_escape failed, using manual replacement: {e}")
|
| 102 |
+
result = text
|
| 103 |
+
result = result.replace('\\n', '\n')
|
| 104 |
+
result = result.replace('\\t', '\t')
|
| 105 |
+
result = result.replace('\\r', '\r')
|
| 106 |
+
result = result.replace('\\"', '"')
|
| 107 |
+
result = result.replace("\\'", "'")
|
| 108 |
+
result = result.replace('\\\\', '\\')
|
| 109 |
+
return result
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# Pydantic models for inputs
|
| 113 |
+
class LoadModelRequest(BaseModel):
|
| 114 |
+
model_path: str = "Qwen/Qwen3-0.6B"
|
| 115 |
+
quantization_4bit: bool = False # Default to False to avoid bitsandbytes requirement
|
| 116 |
+
dtype: str = "float16" # float16, bfloat16, float32, auto
|
| 117 |
+
revision: Optional[str] = None
|
| 118 |
+
# LRP is no longer loaded at model load time
|
| 119 |
+
|
| 120 |
+
class ComputeLogitsRequest(BaseModel):
|
| 121 |
+
prompt: str
|
| 122 |
+
is_append_bos: bool = True
|
| 123 |
+
topk: int = 10
|
| 124 |
+
extra_token_ids: Optional[List[int]] = None
|
| 125 |
+
extra_token_strs: Optional[List[str]] = None
|
| 126 |
+
capture_mid: bool = False # Fine-grained attribution separation
|
| 127 |
+
|
| 128 |
+
class BackpropConfig(BaseModel):
|
| 129 |
+
mode: str = "max_logit" # "max_logit" or "logit_diff"
|
| 130 |
+
strategy: Optional[str] = "by_topk_avg" # "demean", "by_topk_avg", "by_ref_token"
|
| 131 |
+
ref_token_id: Optional[int] = None
|
| 132 |
+
contrast_rank: Optional[int] = 2
|
| 133 |
+
k: Optional[int] = 10
|
| 134 |
+
node_threshold: Optional[float] = 0.01 # Threshold for computing node inter-connections
|
| 135 |
+
|
| 136 |
+
class ComputeCircuitRequest(BaseModel):
|
| 137 |
+
# Configurations for backprop
|
| 138 |
+
backprop_config: BackpropConfig
|
| 139 |
+
|
| 140 |
+
# New Multi-Layer Field
|
| 141 |
+
layers: List[int]
|
| 142 |
+
|
| 143 |
+
# Pruning Params
|
| 144 |
+
pruning_mode: str = "by_per_layer_cum_mass_percentile"
|
| 145 |
+
top_p: float = 0.9
|
| 146 |
+
edge_threshold: float = 0.01 # Used if by_global_threshold
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class ComputeInputAttributionRequest(BaseModel):
|
| 150 |
+
target_token_id: int
|
| 151 |
+
contrast_token_id: Optional[int] = None
|
| 152 |
+
backprop_config: BackpropConfig
|
| 153 |
+
|
| 154 |
+
class GenerateRequest(BaseModel):
|
| 155 |
+
prompt: str
|
| 156 |
+
max_new_tokens: int = 30
|
| 157 |
+
append_token_id: Optional[int] = None
|
| 158 |
+
|
| 159 |
+
class LocateErrorTokenRequest(BaseModel):
|
| 160 |
+
prompt: str
|
| 161 |
+
completion: str
|
| 162 |
+
ground_truth: Optional[str] = None
|
| 163 |
+
validators: Optional[List[str]] = None
|
| 164 |
+
use_llm: bool = True
|
| 165 |
+
manual_chunks: Optional[List[str]] = None
|
| 166 |
+
|
| 167 |
+
class EnableLRPRequest(BaseModel):
|
| 168 |
+
lrp_rule: str = "Attn-LRP" # "Attn-LRP", "CP-LRP", or "Gradient"
|
| 169 |
+
capture_mid: bool = False
|
| 170 |
+
|
| 171 |
+
class ComputePerturbationRequest(BaseModel):
|
| 172 |
+
attribution_scores: List[float]
|
| 173 |
+
k_values: List[int] = [1, 3, 5, 10]
|
| 174 |
+
target_token_id: int
|
| 175 |
+
|
| 176 |
+
class ComputePerturbationManualRequest(BaseModel):
|
| 177 |
+
perturb_indices: List[int]
|
| 178 |
+
target_token_id: int
|
| 179 |
+
|
| 180 |
+
@app.get("/api/list_hf_models")
|
| 181 |
+
async def list_hf_models(series: str = "Qwen2"):
|
| 182 |
+
"""
|
| 183 |
+
List models from HuggingFace Hub filtered by series/author.
|
| 184 |
+
"""
|
| 185 |
+
try:
|
| 186 |
+
if series.lower() == "qwen2":
|
| 187 |
+
models = list(list_models(author="Qwen", search="Qwen2", filter="text-generation", sort="downloads", direction=-1, limit=50))
|
| 188 |
+
return {"models": [m.id for m in models]}
|
| 189 |
+
|
| 190 |
+
elif series.lower() == "qwen3":
|
| 191 |
+
models = list(list_models(author="Qwen", search="Qwen3", filter="text-generation", sort="downloads", direction=-1, limit=50))
|
| 192 |
+
return {"models": [m.id for m in models]}
|
| 193 |
+
|
| 194 |
+
elif series.lower() == "olmo3":
|
| 195 |
+
models = list(list_models(author="allenai", search="Olmo-3", filter="text-generation", sort="downloads", direction=-1, limit=50))
|
| 196 |
+
return {"models": [m.id for m in models]}
|
| 197 |
+
|
| 198 |
+
elif series.lower() == "olmo":
|
| 199 |
+
models = list(list_models(author="allenai", search="OLMo", filter="text-generation", sort="downloads", direction=-1, limit=50))
|
| 200 |
+
return {"models": [m.id for m in models]}
|
| 201 |
+
|
| 202 |
+
elif series.lower() == "qwen":
|
| 203 |
+
models = list(list_models(author="Qwen", filter="text-generation", sort="downloads", direction=-1, limit=50))
|
| 204 |
+
return {"models": [m.id for m in models]}
|
| 205 |
+
|
| 206 |
+
# Generic fallback
|
| 207 |
+
models = list(list_models(search=series, filter="text-generation", sort="downloads", direction=-1, limit=20))
|
| 208 |
+
return {"models": [m.id for m in models]}
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
print(f"Error listing models: {e}")
|
| 212 |
+
# Return fallback/hardcoded list if offline
|
| 213 |
+
if series.lower() == "qwen2":
|
| 214 |
+
return {"models": ["Qwen/Qwen2.5-0.5B-Instruct", "Qwen/Qwen2.5-1.5B-Instruct", "Qwen/Qwen2.5-3B-Instruct", "Qwen/Qwen2.5-7B-Instruct", "Qwen/Qwen2-0.5B", "Qwen/Qwen2-1.5B", "Qwen/Qwen2-7B"]}
|
| 215 |
+
elif series.lower() == "qwen3":
|
| 216 |
+
return {"models": ["Qwen/Qwen3-0.6B"]}
|
| 217 |
+
elif series.lower() == "qwen":
|
| 218 |
+
return {"models": ["Qwen/Qwen2.5-0.5B-Instruct", "Qwen/Qwen3-0.6B"]}
|
| 219 |
+
elif series.lower() == "olmo3":
|
| 220 |
+
return {"models": ["allenai/Olmo-3-7B-Think"]}
|
| 221 |
+
elif series.lower() == "olmo":
|
| 222 |
+
return {"models": ["allenai/OLMo-7B", "allenai/OLMo-1B-0724", "allenai/Olmo-3-7B-Think"]}
|
| 223 |
+
return {"models": [], "error": str(e)}
|
| 224 |
+
|
| 225 |
+
@app.get("/api/list_model_revisions")
|
| 226 |
+
async def list_model_revisions(model_id: str):
|
| 227 |
+
"""
|
| 228 |
+
List git branches/refs for a model.
|
| 229 |
+
"""
|
| 230 |
+
try:
|
| 231 |
+
refs = list_repo_refs(model_id)
|
| 232 |
+
branches = [b.name for b in refs.branches]
|
| 233 |
+
tags = [t.name for t in refs.tags]
|
| 234 |
+
return {"branches": branches, "tags": tags}
|
| 235 |
+
except Exception as e:
|
| 236 |
+
print(f"Error listing revisions for {model_id}: {e}")
|
| 237 |
+
return {"branches": [], "tags": [], "error": str(e)}
|
| 238 |
+
|
| 239 |
+
@app.post("/api/cleanup")
|
| 240 |
+
async def cleanup_memory():
|
| 241 |
+
global attribution_engine
|
| 242 |
+
if attribution_engine:
|
| 243 |
+
attribution_engine.reset()
|
| 244 |
+
else:
|
| 245 |
+
# even if no engine, try to clear cache
|
| 246 |
+
torch.cuda.empty_cache()
|
| 247 |
+
|
| 248 |
+
import gc
|
| 249 |
+
gc.collect()
|
| 250 |
+
|
| 251 |
+
return {"status": "success", "message": "Memory cleanup complete"}
|
| 252 |
+
|
| 253 |
+
@app.post("/api/generate")
|
| 254 |
+
async def generate_continuation(request: GenerateRequest):
|
| 255 |
+
if not model_manager.model:
|
| 256 |
+
raise HTTPException(status_code=400, detail="Model not loaded")
|
| 257 |
+
|
| 258 |
+
tokenizer = model_manager.tokenizer
|
| 259 |
+
model = model_manager.model
|
| 260 |
+
device = model_manager.device
|
| 261 |
+
|
| 262 |
+
try:
|
| 263 |
+
# Unescape special characters in prompt
|
| 264 |
+
prompt = unescape_string(request.prompt)
|
| 265 |
+
|
| 266 |
+
# Switch to eval for generation
|
| 267 |
+
was_training = model.training
|
| 268 |
+
model.eval()
|
| 269 |
+
|
| 270 |
+
# Encode prompt
|
| 271 |
+
input_ids = tokenizer.encode(prompt, return_tensors="pt").to(device)
|
| 272 |
+
|
| 273 |
+
# Append token if requested
|
| 274 |
+
if request.append_token_id is not None:
|
| 275 |
+
token_tensor = torch.tensor([[request.append_token_id]], device=device)
|
| 276 |
+
input_ids = torch.cat([input_ids, token_tensor], dim=1)
|
| 277 |
+
|
| 278 |
+
with torch.no_grad():
|
| 279 |
+
output_ids = model.generate(
|
| 280 |
+
input_ids,
|
| 281 |
+
max_new_tokens=request.max_new_tokens,
|
| 282 |
+
do_sample=False,
|
| 283 |
+
pad_token_id=tokenizer.eos_token_id
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
new_token_ids = output_ids[0][input_ids.shape[1]:]
|
| 287 |
+
generated_text = tokenizer.decode(new_token_ids, skip_special_tokens=True)
|
| 288 |
+
|
| 289 |
+
# Restore training mode
|
| 290 |
+
if was_training:
|
| 291 |
+
model.train()
|
| 292 |
+
|
| 293 |
+
return {"generated_text": generated_text}
|
| 294 |
+
|
| 295 |
+
except Exception as e:
|
| 296 |
+
if model_manager.model and was_training:
|
| 297 |
+
model_manager.model.train()
|
| 298 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 299 |
+
|
| 300 |
+
@app.post("/api/locate_err_token")
|
| 301 |
+
async def locate_error_token_endpoint(request: LocateErrorTokenRequest):
|
| 302 |
+
"""
|
| 303 |
+
Locate the error token in a completion using multiple LLM validators
|
| 304 |
+
"""
|
| 305 |
+
global error_token_locator
|
| 306 |
+
|
| 307 |
+
if not error_token_locator:
|
| 308 |
+
raise HTTPException(status_code=400, detail="Model not loaded. Please call /api/load_model first.")
|
| 309 |
+
|
| 310 |
+
try:
|
| 311 |
+
# Unescape special characters in prompt, completion, and ground_truth
|
| 312 |
+
prompt = unescape_string(request.prompt)
|
| 313 |
+
completion = unescape_string(request.completion)
|
| 314 |
+
ground_truth = unescape_string(request.ground_truth) if request.ground_truth else None
|
| 315 |
+
|
| 316 |
+
# Call the error token locator
|
| 317 |
+
result = error_token_locator.locate_error_token(
|
| 318 |
+
prompt=prompt,
|
| 319 |
+
completion=completion,
|
| 320 |
+
ground_truth=ground_truth,
|
| 321 |
+
validators=request.validators,
|
| 322 |
+
use_llm=request.use_llm,
|
| 323 |
+
manual_chunks=request.manual_chunks
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
if result["status"] == "error":
|
| 327 |
+
raise HTTPException(status_code=500, detail=result.get("message", "Unknown error"))
|
| 328 |
+
|
| 329 |
+
return {
|
| 330 |
+
"status": "success",
|
| 331 |
+
"truncated_text": result["truncated_text"],
|
| 332 |
+
"explanation": result["explanation"],
|
| 333 |
+
"error_token_index": result.get("error_token_index", -1),
|
| 334 |
+
"vote_details": result.get("vote_details", {})
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
except HTTPException:
|
| 338 |
+
raise
|
| 339 |
+
except Exception as e:
|
| 340 |
+
import traceback
|
| 341 |
+
traceback.print_exc()
|
| 342 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 343 |
+
|
| 344 |
+
@app.post("/api/load_model")
|
| 345 |
+
async def load_model(request: LoadModelRequest):
|
| 346 |
+
global attribution_engine
|
| 347 |
+
global error_token_locator
|
| 348 |
+
try:
|
| 349 |
+
# Load model without LRP (LRP will be enabled when needed)
|
| 350 |
+
model_name = model_manager.load_model(
|
| 351 |
+
request.model_path,
|
| 352 |
+
request.quantization_4bit,
|
| 353 |
+
dtype=request.dtype,
|
| 354 |
+
revision=request.revision,
|
| 355 |
+
lrp_rule=None # Don't load LRP yet
|
| 356 |
+
)
|
| 357 |
+
attribution_engine = AttributionEngine(model_manager)
|
| 358 |
+
error_token_locator = ErrorTokenLocator(model_manager.model, model_manager.tokenizer)
|
| 359 |
+
|
| 360 |
+
# Get Num Layers
|
| 361 |
+
n_layers = 28 # Default for Qwen 0.5B
|
| 362 |
+
try:
|
| 363 |
+
# Try access config
|
| 364 |
+
if hasattr(model_manager.model, 'config'):
|
| 365 |
+
n_layers = getattr(model_manager.model.config, 'num_hidden_layers', 28)
|
| 366 |
+
except:
|
| 367 |
+
pass
|
| 368 |
+
|
| 369 |
+
# Get vocabulary size
|
| 370 |
+
vocab_size = len(model_manager.tokenizer)
|
| 371 |
+
|
| 372 |
+
return {
|
| 373 |
+
"status": "success",
|
| 374 |
+
"message": f"Model {model_name} loaded successfully",
|
| 375 |
+
"num_layers": n_layers,
|
| 376 |
+
"vocab_size": vocab_size
|
| 377 |
+
}
|
| 378 |
+
except Exception as e:
|
| 379 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 380 |
+
|
| 381 |
+
@app.post("/api/enable_lrp")
|
| 382 |
+
async def enable_lrp(request: EnableLRPRequest):
|
| 383 |
+
"""
|
| 384 |
+
Enable LRP functionality on the loaded model.
|
| 385 |
+
This should be called before computing attribution or circuits.
|
| 386 |
+
For "Gradient" rule, the model is loaded WITHOUT LRP patches (vanilla gradient).
|
| 387 |
+
"""
|
| 388 |
+
if not model_manager.model:
|
| 389 |
+
raise HTTPException(status_code=400, detail="Model not loaded. Please call /api/load_model first.")
|
| 390 |
+
|
| 391 |
+
try:
|
| 392 |
+
# For Gradient method, load model without LRP patches
|
| 393 |
+
lrp_rule_for_model = None if request.lrp_rule == "Gradient" else request.lrp_rule
|
| 394 |
+
|
| 395 |
+
# Reload model with appropriate LRP setting
|
| 396 |
+
model_name = model_manager.load_model(
|
| 397 |
+
model_path=model_manager.current_model_path,
|
| 398 |
+
quantization_4bit=model_manager.current_quantization,
|
| 399 |
+
dtype=model_manager.current_dtype,
|
| 400 |
+
revision=model_manager.current_revision,
|
| 401 |
+
lrp_rule=lrp_rule_for_model
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Reinitialize attribution engine
|
| 405 |
+
global attribution_engine
|
| 406 |
+
attribution_engine = AttributionEngine(model_manager)
|
| 407 |
+
|
| 408 |
+
return {
|
| 409 |
+
"status": "success",
|
| 410 |
+
"message": f"Attribution method ({request.lrp_rule}) enabled successfully",
|
| 411 |
+
"lrp_rule": request.lrp_rule,
|
| 412 |
+
"capture_mid": request.capture_mid
|
| 413 |
+
}
|
| 414 |
+
except Exception as e:
|
| 415 |
+
import traceback
|
| 416 |
+
traceback.print_exc()
|
| 417 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 418 |
+
|
| 419 |
+
@app.post("/api/compute_logits")
|
| 420 |
+
async def compute_logits(request: ComputeLogitsRequest):
|
| 421 |
+
global attribution_engine
|
| 422 |
+
if not attribution_engine:
|
| 423 |
+
raise HTTPException(status_code=400, detail="Model not loaded. Please call /api/load_model first.")
|
| 424 |
+
|
| 425 |
+
try:
|
| 426 |
+
# Unescape special characters in prompt
|
| 427 |
+
prompt = unescape_string(request.prompt)
|
| 428 |
+
|
| 429 |
+
topk_data, _, input_tokens = attribution_engine.compute_logits(
|
| 430 |
+
prompt=prompt,
|
| 431 |
+
is_append_bos=request.is_append_bos,
|
| 432 |
+
topk=request.topk,
|
| 433 |
+
extra_token_ids=request.extra_token_ids,
|
| 434 |
+
extra_token_strs=request.extra_token_strs,
|
| 435 |
+
capture_mid=request.capture_mid
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
# Convert simple string list input_tokens to list of objects for frontend consistency
|
| 439 |
+
token_objs = [{"token_str": t, "token_id": i} for i, t in enumerate(input_tokens)]
|
| 440 |
+
|
| 441 |
+
return {"data": topk_data, "tokens": token_objs}
|
| 442 |
+
except Exception as e:
|
| 443 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 444 |
+
|
| 445 |
+
@app.post("/api/compute_input_attribution")
|
| 446 |
+
async def compute_input_attribution_endpoint(request: ComputeInputAttributionRequest):
|
| 447 |
+
global attribution_engine
|
| 448 |
+
if not attribution_engine:
|
| 449 |
+
raise HTTPException(status_code=400, detail="Model not loaded.")
|
| 450 |
+
|
| 451 |
+
# Auto-enable LRP if not already enabled
|
| 452 |
+
if not model_manager.current_lrp_rule:
|
| 453 |
+
logger.info("LRP not enabled yet - auto-enabling with default rule 'Attn-LRP'...")
|
| 454 |
+
try:
|
| 455 |
+
model_name = model_manager.load_model(
|
| 456 |
+
model_path=model_manager.current_model_path,
|
| 457 |
+
quantization_4bit=model_manager.current_quantization,
|
| 458 |
+
dtype=model_manager.current_dtype,
|
| 459 |
+
revision=model_manager.current_revision,
|
| 460 |
+
lrp_rule="Attn-LRP"
|
| 461 |
+
)
|
| 462 |
+
attribution_engine = AttributionEngine(model_manager)
|
| 463 |
+
logger.info(f"Auto-enabled LRP with Attn-LRP rule on {model_name}")
|
| 464 |
+
except Exception as e:
|
| 465 |
+
logger.error(f"Failed to auto-enable LRP: {e}")
|
| 466 |
+
raise HTTPException(
|
| 467 |
+
status_code=400,
|
| 468 |
+
detail="LRP not enabled and auto-enable failed. Please call /api/enable_lrp before computing attribution."
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
try:
|
| 472 |
+
if attribution_engine.outputs is None:
|
| 473 |
+
raise HTTPException(status_code=400, detail="No forward pass found. Run compute_logits first.")
|
| 474 |
+
|
| 475 |
+
# Inject target token ID into backprop config
|
| 476 |
+
bp_config = request.backprop_config.dict()
|
| 477 |
+
bp_config["target_token_id"] = request.target_token_id
|
| 478 |
+
|
| 479 |
+
relevance = attribution_engine.compute_input_attribution(bp_config)
|
| 480 |
+
return {"relevance": relevance}
|
| 481 |
+
except Exception as e:
|
| 482 |
+
import traceback
|
| 483 |
+
traceback.print_exc()
|
| 484 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 485 |
+
|
| 486 |
+
@app.post("/api/compute_input_attribution_gradient")
|
| 487 |
+
async def compute_input_attribution_gradient_endpoint(request: ComputeInputAttributionRequest):
|
| 488 |
+
"""
|
| 489 |
+
Compute input attribution using vanilla gradient method (Input * Gradient).
|
| 490 |
+
Does NOT require LRP to be enabled - uses standard PyTorch autograd.
|
| 491 |
+
"""
|
| 492 |
+
global attribution_engine
|
| 493 |
+
if not attribution_engine:
|
| 494 |
+
raise HTTPException(status_code=400, detail="Model not loaded.")
|
| 495 |
+
|
| 496 |
+
try:
|
| 497 |
+
if attribution_engine.outputs is None:
|
| 498 |
+
raise HTTPException(status_code=400, detail="No forward pass found. Run compute_logits first.")
|
| 499 |
+
|
| 500 |
+
# Inject target token ID into backprop config
|
| 501 |
+
bp_config = request.backprop_config.dict()
|
| 502 |
+
bp_config["target_token_id"] = request.target_token_id
|
| 503 |
+
|
| 504 |
+
relevance = attribution_engine.compute_input_attribution_gradient(bp_config)
|
| 505 |
+
return {"relevance": relevance}
|
| 506 |
+
except Exception as e:
|
| 507 |
+
import traceback
|
| 508 |
+
traceback.print_exc()
|
| 509 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 510 |
+
|
| 511 |
+
@app.post("/api/compute_perturbation")
|
| 512 |
+
async def compute_perturbation_endpoint(request: ComputePerturbationRequest):
|
| 513 |
+
"""
|
| 514 |
+
Evaluate attribution quality by perturbing top-attributed tokens.
|
| 515 |
+
Zero out top-k most attributed tokens and check if the error is fixed.
|
| 516 |
+
"""
|
| 517 |
+
global attribution_engine
|
| 518 |
+
if not attribution_engine:
|
| 519 |
+
raise HTTPException(status_code=400, detail="Model not loaded.")
|
| 520 |
+
|
| 521 |
+
try:
|
| 522 |
+
if attribution_engine.input_ids is None or attribution_engine.input_embeddings is None:
|
| 523 |
+
raise HTTPException(status_code=400, detail="No forward pass found. Run compute_logits first.")
|
| 524 |
+
|
| 525 |
+
results = attribution_engine.compute_perturbation_eval(
|
| 526 |
+
attribution_scores=request.attribution_scores,
|
| 527 |
+
k_values=request.k_values,
|
| 528 |
+
target_token_id=request.target_token_id
|
| 529 |
+
)
|
| 530 |
+
return {"results": results}
|
| 531 |
+
except Exception as e:
|
| 532 |
+
import traceback
|
| 533 |
+
traceback.print_exc()
|
| 534 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 535 |
+
|
| 536 |
+
@app.post("/api/compute_perturbation_manual")
|
| 537 |
+
async def compute_perturbation_manual_endpoint(request: ComputePerturbationManualRequest):
|
| 538 |
+
"""
|
| 539 |
+
Evaluate attribution by perturbing manually selected token positions.
|
| 540 |
+
Zero out the specified token embeddings and check if the error is fixed.
|
| 541 |
+
"""
|
| 542 |
+
global attribution_engine
|
| 543 |
+
if not attribution_engine:
|
| 544 |
+
raise HTTPException(status_code=400, detail="Model not loaded.")
|
| 545 |
+
|
| 546 |
+
try:
|
| 547 |
+
if attribution_engine.input_ids is None or attribution_engine.input_embeddings is None:
|
| 548 |
+
raise HTTPException(status_code=400, detail="No forward pass found. Run compute_logits first.")
|
| 549 |
+
|
| 550 |
+
result = attribution_engine.compute_perturbation_manual(
|
| 551 |
+
perturb_indices=request.perturb_indices,
|
| 552 |
+
target_token_id=request.target_token_id
|
| 553 |
+
)
|
| 554 |
+
return {"result": result}
|
| 555 |
+
except Exception as e:
|
| 556 |
+
import traceback
|
| 557 |
+
traceback.print_exc()
|
| 558 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 559 |
+
|
| 560 |
+
@app.post("/api/compute_circuit")
|
| 561 |
+
async def compute_circuit(request: ComputeCircuitRequest):
|
| 562 |
+
global attribution_engine
|
| 563 |
+
if not attribution_engine:
|
| 564 |
+
raise HTTPException(status_code=400, detail="Model not loaded.")
|
| 565 |
+
|
| 566 |
+
# Auto-enable LRP if not already enabled
|
| 567 |
+
if not model_manager.current_lrp_rule:
|
| 568 |
+
logger.info("LRP not enabled yet - auto-enabling with default rule 'Attn-LRP' for circuit analysis...")
|
| 569 |
+
try:
|
| 570 |
+
model_name = model_manager.load_model(
|
| 571 |
+
model_path=model_manager.current_model_path,
|
| 572 |
+
quantization_4bit=model_manager.current_quantization,
|
| 573 |
+
dtype=model_manager.current_dtype,
|
| 574 |
+
revision=model_manager.current_revision,
|
| 575 |
+
lrp_rule="Attn-LRP"
|
| 576 |
+
)
|
| 577 |
+
attribution_engine = AttributionEngine(model_manager)
|
| 578 |
+
logger.info(f"Auto-enabled LRP with Attn-LRP rule on {model_name}")
|
| 579 |
+
except Exception as e:
|
| 580 |
+
logger.error(f"Failed to auto-enable LRP: {e}")
|
| 581 |
+
raise HTTPException(
|
| 582 |
+
status_code=400,
|
| 583 |
+
detail="LRP not enabled and auto-enable failed. Please call /api/enable_lrp before computing circuits."
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
if attribution_engine.outputs is None:
|
| 587 |
+
raise HTTPException(status_code=400, detail="No forward pass found. Run compute_logits first.")
|
| 588 |
+
|
| 589 |
+
async def generate_response():
|
| 590 |
+
try:
|
| 591 |
+
# Step 1: Run Backward Pass
|
| 592 |
+
yield json.dumps({"type": "progress", "msg": "Initiating Backward Pass...", "percent": 0}) + "\n"
|
| 593 |
+
|
| 594 |
+
# Use CircuitAnalyzer
|
| 595 |
+
analyzer = CircuitAnalyzer(attribution_engine)
|
| 596 |
+
|
| 597 |
+
bp_config = request.backprop_config.dict()
|
| 598 |
+
|
| 599 |
+
# We explicitly run backward pass first (though build_graph does it, we want to emit progress)
|
| 600 |
+
# Check Cache
|
| 601 |
+
current_hash = get_config_hash(bp_config, request.layers)
|
| 602 |
+
connection_data = None
|
| 603 |
+
|
| 604 |
+
if CACHED_CONNECTION_DATA["config_hash"] == current_hash and CACHED_CONNECTION_DATA["data"] is not None:
|
| 605 |
+
yield json.dumps({"type": "progress", "msg": "Using Cached Matrices (Fast)...", "percent": 50}) + "\n"
|
| 606 |
+
connection_data = CACHED_CONNECTION_DATA["data"]
|
| 607 |
+
else:
|
| 608 |
+
yield json.dumps({"type": "progress", "msg": "Computing Circuit (This may take a moment)...", "percent": 20}) + "\n"
|
| 609 |
+
# Run the heavy lifting
|
| 610 |
+
connection_data = analyzer.compute_connection_matrices(bp_config, sorted(request.layers))
|
| 611 |
+
|
| 612 |
+
# Update Cache
|
| 613 |
+
CACHED_CONNECTION_DATA["config_hash"] = current_hash
|
| 614 |
+
CACHED_CONNECTION_DATA["data"] = connection_data
|
| 615 |
+
|
| 616 |
+
yield json.dumps({"type": "progress", "msg": "Pruning & Building Graph...", "percent": 80}) + "\n"
|
| 617 |
+
|
| 618 |
+
G, pruning_details = analyzer.build_graph_from_matrices(
|
| 619 |
+
connection_data,
|
| 620 |
+
edge_rel_threshold=request.edge_threshold,
|
| 621 |
+
pruning_mode=request.pruning_mode,
|
| 622 |
+
top_p=request.top_p
|
| 623 |
+
)
|
| 624 |
+
|
| 625 |
+
yield json.dumps({"type": "progress", "msg": "Graph Constructed. Serializing...", "percent": 90}) + "\n"
|
| 626 |
+
|
| 627 |
+
# Serialize Graph
|
| 628 |
+
graph_data = nx.node_link_data(G)
|
| 629 |
+
|
| 630 |
+
yield json.dumps({
|
| 631 |
+
"type": "graph_data",
|
| 632 |
+
"graph": graph_data,
|
| 633 |
+
"pruning_details": pruning_details
|
| 634 |
+
}, cls=NumpyEncoder) + "\n"
|
| 635 |
+
|
| 636 |
+
yield json.dumps({"type": "progress", "msg": "Complete!", "percent": 100}) + "\n"
|
| 637 |
+
yield json.dumps({"type": "complete"}) + "\n"
|
| 638 |
+
|
| 639 |
+
except Exception as e:
|
| 640 |
+
import traceback
|
| 641 |
+
traceback.print_exc()
|
| 642 |
+
yield json.dumps({"type": "error", "msg": str(e)}) + "\n"
|
| 643 |
+
|
| 644 |
+
return StreamingResponse(generate_response(), media_type="application/x-ndjson")
|
| 645 |
+
|
| 646 |
+
# Return empty datasets (dataset loading removed for HuggingFace Space deployment)
|
| 647 |
+
@app.get("/api/datasets")
|
| 648 |
+
async def get_datasets():
|
| 649 |
+
return {"datasets": []}
|
| 650 |
+
|
| 651 |
+
if __name__ == "__main__":
|
| 652 |
+
port = int(os.environ.get("PORT", 7860))
|
| 653 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
backend/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .models import ModelManager
|
| 2 |
+
from .core import AttributionEngine
|
backend/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (229 Bytes). View file
|
|
|
backend/__pycache__/batch_config.cpython-312.pyc
ADDED
|
Binary file (1.21 kB). View file
|
|
|
backend/__pycache__/circuit.cpython-312.pyc
ADDED
|
Binary file (10.3 kB). View file
|
|
|
backend/__pycache__/core.cpython-312.pyc
ADDED
|
Binary file (34 kB). View file
|
|
|
backend/__pycache__/error_token_location.cpython-312.pyc
ADDED
|
Binary file (12.4 kB). View file
|
|
|
backend/__pycache__/graph_metrics.cpython-312.pyc
ADDED
|
Binary file (15.4 kB). View file
|
|
|
backend/__pycache__/metrics.cpython-312.pyc
ADDED
|
Binary file (5.4 kB). View file
|
|
|
backend/batch_config.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
# Configuration for Batch Chunk Sizes based on Model Size and Dtype
|
| 4 |
+
# Reference: 4B model with bf16 uses batch_chunk_size = 64
|
| 5 |
+
|
| 6 |
+
# Mapping: (Min_Params_Billions, Max_Params_Billions) -> Recommended Batch Size (BF16/FP16)
|
| 7 |
+
BATCH_CHUNK_MAPPING = {
|
| 8 |
+
(0.0, 1.0): 256, # For 0.6B and similar
|
| 9 |
+
(1.0, 3.0): 128, # For 1.7B, 2B, 3B
|
| 10 |
+
(3.0, 6.0): 8, # For 4B, 6B
|
| 11 |
+
(6.0, 12.0): 32, # For 7B, 8B, 10B
|
| 12 |
+
(12.0, 25.0): 16, # For 14B, 20B
|
| 13 |
+
(25.0, 1000.0): 8 # For 32B+, 70B
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
def get_batch_chunk_size(model_params_count, model_dtype):
|
| 17 |
+
"""
|
| 18 |
+
Determine appropriate batch chunk size based on parameter count and dtype.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
model_params_count (int): Total number of parameters in the model.
|
| 22 |
+
model_dtype (torch.dtype): The data type used for computation (activations).
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
int: Recommended batch chunk size.
|
| 26 |
+
"""
|
| 27 |
+
# Convert to Billions
|
| 28 |
+
params_billions = model_params_count / 1e9
|
| 29 |
+
|
| 30 |
+
# Default fallback
|
| 31 |
+
chunk_size = 32
|
| 32 |
+
|
| 33 |
+
# Lookup in mapping
|
| 34 |
+
for (min_b, max_b), size in BATCH_CHUNK_MAPPING.items():
|
| 35 |
+
if min_b <= params_billions < max_b:
|
| 36 |
+
chunk_size = size
|
| 37 |
+
break
|
| 38 |
+
|
| 39 |
+
# Scale by Dtype
|
| 40 |
+
# If using float32, activations take 2x memory compared to bf16/fp16
|
| 41 |
+
if model_dtype == torch.float32:
|
| 42 |
+
chunk_size = max(1, chunk_size // 2)
|
| 43 |
+
|
| 44 |
+
return chunk_size
|
backend/circuit.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import networkx as nx
|
| 2 |
+
import torch
|
| 3 |
+
import gc
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pandas as pd
|
| 6 |
+
|
| 7 |
+
class CircuitAnalyzer:
|
| 8 |
+
"""
|
| 9 |
+
A class to build and analyze attribution circuits using an AttributionEngine.
|
| 10 |
+
"""
|
| 11 |
+
def __init__(self, attribution_engine):
|
| 12 |
+
"""
|
| 13 |
+
Args:
|
| 14 |
+
attribution_engine: An instance of AttributionEngine.
|
| 15 |
+
"""
|
| 16 |
+
self.engine = attribution_engine
|
| 17 |
+
|
| 18 |
+
def compute_connection_matrices(self, backprop_config, layers=None):
|
| 19 |
+
"""
|
| 20 |
+
Step 1: Compute dense connection matrices for all layer transitions.
|
| 21 |
+
Returns a list of dictionaries containing matrix data for each transition.
|
| 22 |
+
"""
|
| 23 |
+
model = self.engine.manager.get_model()
|
| 24 |
+
model_layers = model.model.layers
|
| 25 |
+
n_layers = len(model_layers)
|
| 26 |
+
|
| 27 |
+
if layers is None:
|
| 28 |
+
# Auto-detect if mid activations are available (captured in compute_logits)
|
| 29 |
+
check_layer = model_layers[0]
|
| 30 |
+
has_mid = False
|
| 31 |
+
# Check for mid_activation attribute on the normative layer where we hook
|
| 32 |
+
if hasattr(check_layer, 'post_attention_layernorm') and hasattr(check_layer.post_attention_layernorm, 'mid_activation'):
|
| 33 |
+
has_mid = True
|
| 34 |
+
print("Auto-detected mid activations: Using fine-grained circuit (Attn/MLP separation).")
|
| 35 |
+
|
| 36 |
+
nodes = [-1]
|
| 37 |
+
for i in range(n_layers):
|
| 38 |
+
if has_mid:
|
| 39 |
+
nodes.append((i, 'mid'))
|
| 40 |
+
nodes.append(i) # integers imply 'post'
|
| 41 |
+
|
| 42 |
+
# Sort/Sequence is crucial.
|
| 43 |
+
# [-1, (0,'mid'), 0, (1,'mid'), 1, ...]
|
| 44 |
+
layer_pairs = list(zip(nodes[:-1], nodes[1:]))
|
| 45 |
+
else:
|
| 46 |
+
# Use provided layers list
|
| 47 |
+
# Ensure it's sorted? User responsibility if custom list.
|
| 48 |
+
nodes = layers
|
| 49 |
+
layer_pairs = list(zip(nodes[:-1], nodes[1:]))
|
| 50 |
+
|
| 51 |
+
# 1. Run Backward Pass
|
| 52 |
+
print("Running backward pass...")
|
| 53 |
+
self.engine.run_backward_pass(backprop_config)
|
| 54 |
+
|
| 55 |
+
connection_data = []
|
| 56 |
+
|
| 57 |
+
print(f"Computing matrices for {len(nodes)} nodes ({len(layer_pairs)} transitions)...")
|
| 58 |
+
|
| 59 |
+
for i, (src_layer, tgt_layer) in enumerate(layer_pairs):
|
| 60 |
+
print(f"Computing transition: {src_layer} -> {tgt_layer}...")
|
| 61 |
+
|
| 62 |
+
gen = self.engine.compute_connection_matrix_gen(
|
| 63 |
+
source=src_layer,
|
| 64 |
+
target=tgt_layer
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
final_res = None
|
| 68 |
+
for item in gen:
|
| 69 |
+
if item['type'] == 'result':
|
| 70 |
+
final_res = item['payload']
|
| 71 |
+
|
| 72 |
+
if final_res:
|
| 73 |
+
# Store the dense matrix and relevance vectors
|
| 74 |
+
connection_data.append({
|
| 75 |
+
'src_layer': src_layer,
|
| 76 |
+
'tgt_layer': tgt_layer,
|
| 77 |
+
'matrix': final_res['matrix'], # Dense numpy array
|
| 78 |
+
'real_source_rel': final_res['real_source_rel'],
|
| 79 |
+
'real_target_rel': final_res['real_target_rel']
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
# Basic cleanup of engine internals, but we keep the matrix in connection_data
|
| 83 |
+
torch.cuda.empty_cache()
|
| 84 |
+
|
| 85 |
+
return connection_data
|
| 86 |
+
|
| 87 |
+
def build_graph_from_matrices(self, connection_data, edge_rel_threshold=0.01, pruning_mode="by_global_threshold", top_p=0.9):
|
| 88 |
+
"""
|
| 89 |
+
Step 2: Prune based on threshold and build NetworkX graph.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
connection_data: List of dictionaries containing matrix data.
|
| 93 |
+
edge_rel_threshold: Threshold for 'by_global_threshold' mode.
|
| 94 |
+
pruning_mode: Pruning strategy. Options:
|
| 95 |
+
1. "by_global_threshold" (default): Prune globally using edge_rel_threshold.
|
| 96 |
+
2. "by_per_layer_cum_mass_percentile": Prune per layer to keep top_p mass.
|
| 97 |
+
top_p: The cumulative mass percentile (0.0-1.0) for "by_per_layer_cum_mass_percentile". Default 0.9.
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
G: The built NetworkX graph.
|
| 101 |
+
pruning_details: A list of dictionaries containing pruning stats per layer pair.
|
| 102 |
+
"""
|
| 103 |
+
G = nx.DiGraph()
|
| 104 |
+
print(f"Building graph from {len(connection_data)} transitions. Mode: {pruning_mode}...")
|
| 105 |
+
|
| 106 |
+
pruning_details = []
|
| 107 |
+
|
| 108 |
+
for data in connection_data:
|
| 109 |
+
src_layer = data['src_layer']
|
| 110 |
+
tgt_layer = data['tgt_layer']
|
| 111 |
+
matrix = data['matrix']
|
| 112 |
+
real_source_rel = data['real_source_rel']
|
| 113 |
+
real_target_rel = data['real_target_rel']
|
| 114 |
+
abs_matrix = np.abs(matrix)
|
| 115 |
+
|
| 116 |
+
# Count only active edges (non-zero) as total_elements for percentage calculation
|
| 117 |
+
nonzero_mask = abs_matrix > 1e-9
|
| 118 |
+
total_elements = np.sum(nonzero_mask)
|
| 119 |
+
|
| 120 |
+
used_threshold = edge_rel_threshold
|
| 121 |
+
|
| 122 |
+
# Determine mask for edges to keep based on mode
|
| 123 |
+
if pruning_mode == "by_per_layer_cum_mass_percentile":
|
| 124 |
+
# Dynamic thresholding per layer
|
| 125 |
+
flattened = np.sort(abs_matrix.flatten())[::-1]
|
| 126 |
+
total_mass = flattened.sum()
|
| 127 |
+
|
| 128 |
+
dynamic_threshold = 1.0 # Default High if empty
|
| 129 |
+
|
| 130 |
+
if total_mass > 1e-12:
|
| 131 |
+
cumsum = np.cumsum(flattened)
|
| 132 |
+
cutoff_mass = total_mass * top_p
|
| 133 |
+
# Find first index where cumsum >= cutoff_mass
|
| 134 |
+
# searchsorted returns insertion point index i such that a[i-1] < v <= a[i]
|
| 135 |
+
# If we want elements up to index k such sum(0..k) >= target
|
| 136 |
+
cutoff_idx = np.searchsorted(cumsum, cutoff_mass)
|
| 137 |
+
|
| 138 |
+
if cutoff_idx >= len(flattened):
|
| 139 |
+
cutoff_idx = len(flattened) - 1
|
| 140 |
+
|
| 141 |
+
dynamic_threshold = flattened[cutoff_idx]
|
| 142 |
+
# Ensure we do not include effective zeros
|
| 143 |
+
if dynamic_threshold < 1e-9:
|
| 144 |
+
dynamic_threshold = 1e-9
|
| 145 |
+
|
| 146 |
+
used_threshold = dynamic_threshold
|
| 147 |
+
rows, cols = np.where(abs_matrix >= dynamic_threshold)
|
| 148 |
+
else:
|
| 149 |
+
# "by_global_threshold"
|
| 150 |
+
rows, cols = np.where(abs_matrix > edge_rel_threshold)
|
| 151 |
+
|
| 152 |
+
# Record pruning details
|
| 153 |
+
num_kept = len(rows)
|
| 154 |
+
percentage = (num_kept / total_elements * 100) if total_elements > 0 else 0
|
| 155 |
+
|
| 156 |
+
pruning_details.append({
|
| 157 |
+
'src_layer': src_layer,
|
| 158 |
+
'tgt_layer': tgt_layer,
|
| 159 |
+
'threshold': float(used_threshold),
|
| 160 |
+
'kept_edges': int(num_kept),
|
| 161 |
+
'total_edges': int(total_elements),
|
| 162 |
+
'kept_percentage': percentage
|
| 163 |
+
})
|
| 164 |
+
|
| 165 |
+
# Add Source Nodes
|
| 166 |
+
for t_idx in np.where(np.abs(real_source_rel) > 0)[0]:
|
| 167 |
+
src_node_id = (src_layer, t_idx)
|
| 168 |
+
if not G.has_node(src_node_id):
|
| 169 |
+
G.add_node(src_node_id, layer=src_layer, token=t_idx, relevance=real_source_rel[t_idx])
|
| 170 |
+
|
| 171 |
+
# Add Target Nodes
|
| 172 |
+
for t_idx in np.where(np.abs(real_target_rel) > 0)[0]:
|
| 173 |
+
tgt_node_id = (tgt_layer, t_idx)
|
| 174 |
+
if not G.has_node(tgt_node_id):
|
| 175 |
+
G.add_node(tgt_node_id, layer=tgt_layer, token=t_idx, relevance=real_target_rel[t_idx])
|
| 176 |
+
|
| 177 |
+
# Add Edges (Sparse)
|
| 178 |
+
# rows, cols calculated above
|
| 179 |
+
weights = matrix[rows, cols]
|
| 180 |
+
|
| 181 |
+
edges_to_add = []
|
| 182 |
+
for r, c, w in zip(rows, cols, weights):
|
| 183 |
+
# Edge direction: Source (col/c) -> Target (row/r)
|
| 184 |
+
u = (src_layer, c)
|
| 185 |
+
v = (tgt_layer, r)
|
| 186 |
+
# Ensure nodes exist (redundant safety check)
|
| 187 |
+
if not G.has_node(u): G.add_node(u, layer=src_layer, token=c, relevance=real_source_rel[c])
|
| 188 |
+
if not G.has_node(v): G.add_node(v, layer=tgt_layer, token=r, relevance=real_target_rel[r])
|
| 189 |
+
|
| 190 |
+
edges_to_add.append((u, v, {'weight': float(w)}))
|
| 191 |
+
|
| 192 |
+
G.add_edges_from(edges_to_add)
|
| 193 |
+
|
| 194 |
+
print(f"Graph construction complete. Nodes: {G.number_of_nodes()}, Edges: {G.number_of_edges()}")
|
| 195 |
+
return G, pruning_details
|
| 196 |
+
|
| 197 |
+
def build_graph(self, backprop_config, layers=None, edge_rel_threshold=0.01, pruning_mode="by_global_threshold", top_p=0.9):
|
| 198 |
+
"""
|
| 199 |
+
Wrapper that performs both steps: compute matrices and build graph.
|
| 200 |
+
"""
|
| 201 |
+
connection_data = self.compute_connection_matrices(backprop_config, layers)
|
| 202 |
+
return self.build_graph_from_matrices(connection_data, edge_rel_threshold, pruning_mode=pruning_mode, top_p=top_p)
|
| 203 |
+
|
| 204 |
+
def get_connected_subgraph(self, G, target_node=None):
|
| 205 |
+
"""
|
| 206 |
+
Extracts the subgraph connected to the target node (backward reachability).
|
| 207 |
+
If target_node is None, it tries to infer the last token at the last layer.
|
| 208 |
+
|
| 209 |
+
Args:
|
| 210 |
+
G: The full attribution graph
|
| 211 |
+
target_node: tuple (layer, token_idx) of the target.
|
| 212 |
+
|
| 213 |
+
Returns:
|
| 214 |
+
(subgraph, target_node): The connected subgraph and the resolved target node.
|
| 215 |
+
"""
|
| 216 |
+
# 1. Identify Target Node
|
| 217 |
+
if target_node is None:
|
| 218 |
+
if G.number_of_nodes() == 0:
|
| 219 |
+
print("Graph is empty.")
|
| 220 |
+
return None, None
|
| 221 |
+
|
| 222 |
+
# Infer: Max layer
|
| 223 |
+
try:
|
| 224 |
+
max_layer = max([n[0] for n in G.nodes()])
|
| 225 |
+
except ValueError:
|
| 226 |
+
print("Error finding max layer.")
|
| 227 |
+
return None, None
|
| 228 |
+
|
| 229 |
+
# Check nodes in max_layer
|
| 230 |
+
nodes_in_last = [n for n in G.nodes() if n[0] == max_layer]
|
| 231 |
+
if not nodes_in_last:
|
| 232 |
+
print(f"No nodes found in layer {max_layer}.")
|
| 233 |
+
return None, None
|
| 234 |
+
|
| 235 |
+
# Max token index
|
| 236 |
+
max_token = max([n[1] for n in nodes_in_last])
|
| 237 |
+
target_node = (max_layer, max_token)
|
| 238 |
+
|
| 239 |
+
print(f"Extracting connected component for Target Node: {target_node}")
|
| 240 |
+
|
| 241 |
+
if not G.has_node(target_node):
|
| 242 |
+
print(f"Target node {target_node} not found in graph (maybe thresholded out?).")
|
| 243 |
+
return None, target_node
|
| 244 |
+
|
| 245 |
+
# 2. Get Ancestors (Backward Reachability)
|
| 246 |
+
ancestors = nx.ancestors(G, target_node)
|
| 247 |
+
ancestors.add(target_node) # Include self
|
| 248 |
+
|
| 249 |
+
subgraph = G.subgraph(ancestors).copy()
|
| 250 |
+
print(f"Connected Subgraph: {subgraph.number_of_nodes()} nodes, {subgraph.number_of_edges()} edges")
|
| 251 |
+
|
| 252 |
+
return subgraph, target_node
|
backend/core.py
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import numpy as np
|
| 3 |
+
from .batch_config import get_batch_chunk_size
|
| 4 |
+
|
| 5 |
+
class AttributionEngine:
|
| 6 |
+
def __init__(self, model_manager):
|
| 7 |
+
self.manager = model_manager
|
| 8 |
+
self.hook_handles = []
|
| 9 |
+
self.outputs = None
|
| 10 |
+
self.input_ids = None
|
| 11 |
+
|
| 12 |
+
def _parse_node(self, node):
|
| 13 |
+
if isinstance(node, int):
|
| 14 |
+
return node, 'post'
|
| 15 |
+
if isinstance(node, (tuple, list)) and len(node) == 2:
|
| 16 |
+
return node[0], node[1]
|
| 17 |
+
raise ValueError(f"Invalid node format: {node}")
|
| 18 |
+
|
| 19 |
+
def _forward_part1(self, layer_module, hidden_states, position_embeddings=None, attention_mask=None):
|
| 20 |
+
"""
|
| 21 |
+
Executes: Norm -> Attn -> Residual Add
|
| 22 |
+
Returns: resid_mid
|
| 23 |
+
"""
|
| 24 |
+
return self.manager.decomposer.forward_part1(layer_module, hidden_states, position_embeddings, attention_mask)
|
| 25 |
+
|
| 26 |
+
def _forward_part2(self, layer_module, hidden_states):
|
| 27 |
+
"""
|
| 28 |
+
Executes: Norm -> MLP -> Residual Add
|
| 29 |
+
Returns: resid_post
|
| 30 |
+
"""
|
| 31 |
+
return self.manager.decomposer.forward_part2(layer_module, hidden_states)
|
| 32 |
+
|
| 33 |
+
def _hook_hidden_activation(self, module, input, output):
|
| 34 |
+
"""
|
| 35 |
+
Hook to save activation and enable gradient retention.
|
| 36 |
+
"""
|
| 37 |
+
if isinstance(output, tuple):
|
| 38 |
+
output = output[0]
|
| 39 |
+
|
| 40 |
+
# Save output to the module for later access
|
| 41 |
+
module.output = output
|
| 42 |
+
if module.output.requires_grad:
|
| 43 |
+
module.output.retain_grad()
|
| 44 |
+
|
| 45 |
+
def _hook_mid_activation(self, module, input, output):
|
| 46 |
+
"""
|
| 47 |
+
Hook to capture resid_mid at the input of post_attention_layernorm.
|
| 48 |
+
"""
|
| 49 |
+
# input is a tuple (tensor,)
|
| 50 |
+
val = input[0]
|
| 51 |
+
# Attach to the module (which is the Norm layer)
|
| 52 |
+
module.mid_activation = val
|
| 53 |
+
if module.mid_activation.requires_grad:
|
| 54 |
+
module.mid_activation.retain_grad()
|
| 55 |
+
|
| 56 |
+
def register_hooks(self, capture_mid=False):
|
| 57 |
+
"""
|
| 58 |
+
Register forward hooks on all model layers.
|
| 59 |
+
"""
|
| 60 |
+
self.remove_hooks() # Clear existing
|
| 61 |
+
model = self.manager.get_model()
|
| 62 |
+
if not model:
|
| 63 |
+
raise ValueError("Model not loaded yet.")
|
| 64 |
+
|
| 65 |
+
# Assuming generic structure model.model.layers (common in HF Qwen, Llama, etc.)
|
| 66 |
+
if hasattr(model, "model") and hasattr(model.model, "layers"):
|
| 67 |
+
layers = model.model.layers
|
| 68 |
+
elif hasattr(model, "layers"): # Some archs
|
| 69 |
+
layers = model.layers
|
| 70 |
+
else:
|
| 71 |
+
layers = []
|
| 72 |
+
|
| 73 |
+
for layer in layers:
|
| 74 |
+
# Hook Output (Resid Post)
|
| 75 |
+
handle = layer.register_forward_hook(self._hook_hidden_activation)
|
| 76 |
+
self.hook_handles.append(handle)
|
| 77 |
+
|
| 78 |
+
# Hook Mid (Resid Mid) - Pre-hook on Post-Attn Norm
|
| 79 |
+
if capture_mid:
|
| 80 |
+
# Use decomposer to find the correct module for mid activation
|
| 81 |
+
mid_module = self.manager.decomposer.get_mid_activation_module(layer)
|
| 82 |
+
|
| 83 |
+
if mid_module:
|
| 84 |
+
# Use forward hook to get input?
|
| 85 |
+
# register_forward_hook receives (module, input, output)
|
| 86 |
+
# input is (resid_mid,)
|
| 87 |
+
handle_mid = mid_module.register_forward_hook(self._hook_mid_activation)
|
| 88 |
+
self.hook_handles.append(handle_mid)
|
| 89 |
+
else:
|
| 90 |
+
print(f"Warning: Could not identify mid-activation module for layer {layer}. Skipping mid hook.")
|
| 91 |
+
|
| 92 |
+
def remove_hooks(self):
|
| 93 |
+
for handle in self.hook_handles:
|
| 94 |
+
handle.remove()
|
| 95 |
+
self.hook_handles = []
|
| 96 |
+
|
| 97 |
+
def reset(self):
|
| 98 |
+
"""
|
| 99 |
+
Clears all internal state and specific temporary data from model layers.
|
| 100 |
+
"""
|
| 101 |
+
self.remove_hooks()
|
| 102 |
+
self.outputs = None
|
| 103 |
+
self.input_ids = None
|
| 104 |
+
|
| 105 |
+
# Manually clear output tensors attached to layers to free graph
|
| 106 |
+
model = self.manager.get_model()
|
| 107 |
+
if model:
|
| 108 |
+
layers = []
|
| 109 |
+
if hasattr(model, "model") and hasattr(model.model, "layers"):
|
| 110 |
+
layers = model.model.layers
|
| 111 |
+
elif hasattr(model, "layers"):
|
| 112 |
+
layers = model.layers
|
| 113 |
+
|
| 114 |
+
for layer in layers:
|
| 115 |
+
if hasattr(layer, 'output'):
|
| 116 |
+
del layer.output
|
| 117 |
+
if hasattr(layer, 'post_attention_layernorm') and hasattr(layer.post_attention_layernorm, 'mid_activation'):
|
| 118 |
+
del layer.post_attention_layernorm.mid_activation
|
| 119 |
+
|
| 120 |
+
torch.cuda.empty_cache()
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def compute_logits(self, prompt, is_append_bos=False, topk=10, extra_token_ids=None, extra_token_strs=None, capture_mid=False):
|
| 124 |
+
"""
|
| 125 |
+
Section 1: Forward pass to get logits and top-k predictions.
|
| 126 |
+
"""
|
| 127 |
+
model = self.manager.get_model()
|
| 128 |
+
tokenizer = self.manager.get_tokenizer()
|
| 129 |
+
|
| 130 |
+
self.register_hooks(capture_mid=capture_mid)
|
| 131 |
+
|
| 132 |
+
# Prepare input
|
| 133 |
+
# We tokenize with add_special_tokens=False to manually control the BOS/Start token
|
| 134 |
+
inputs = tokenizer(prompt, return_tensors="pt", add_special_tokens=False)
|
| 135 |
+
input_ids = inputs.input_ids.to(model.device)
|
| 136 |
+
|
| 137 |
+
if is_append_bos:
|
| 138 |
+
# 1. Try explicit BOS
|
| 139 |
+
bos_id = tokenizer.bos_token_id
|
| 140 |
+
|
| 141 |
+
# 2. Try CLS (BERT-like)
|
| 142 |
+
if bos_id is None:
|
| 143 |
+
bos_id = tokenizer.cls_token_id
|
| 144 |
+
|
| 145 |
+
# 3. Fallback: EOS (Often used as BOS/Separator in Llama/decoder-only models if BOS is missing)
|
| 146 |
+
if bos_id is None:
|
| 147 |
+
bos_id = tokenizer.eos_token_id
|
| 148 |
+
|
| 149 |
+
if bos_id is not None:
|
| 150 |
+
prefix = torch.tensor([[bos_id]], device=model.device)
|
| 151 |
+
input_ids = torch.cat([prefix, input_ids], dim=1)
|
| 152 |
+
print(f"Appended start token ID: {bos_id}")
|
| 153 |
+
else:
|
| 154 |
+
print("Warning: Append BOS requested but no suitable start token (BOS/CLS/EOS) found.")
|
| 155 |
+
|
| 156 |
+
self.input_ids = input_ids
|
| 157 |
+
|
| 158 |
+
# Embedding with gradients required for LRP base
|
| 159 |
+
# We detach and enable gradients so we can compute attribution w.r.t input embeddings
|
| 160 |
+
# even if the model is frozen/quantized.
|
| 161 |
+
self.input_embeddings = model.get_input_embeddings()(self.input_ids).detach()
|
| 162 |
+
self.input_embeddings.requires_grad_(True)
|
| 163 |
+
|
| 164 |
+
# Forward pass
|
| 165 |
+
# output_hidden_states=True is crucial for some LRP methods,
|
| 166 |
+
# though we use hooks for "efficient" method mostly.
|
| 167 |
+
self.outputs = model(
|
| 168 |
+
inputs_embeds=self.input_embeddings,
|
| 169 |
+
use_cache=False
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
output_logits = self.outputs.logits
|
| 173 |
+
last_logits = output_logits[0, -1, :]
|
| 174 |
+
|
| 175 |
+
# Get Top-K
|
| 176 |
+
sorted_logits, sorted_indices = torch.sort(last_logits, dim=-1, descending=True)
|
| 177 |
+
|
| 178 |
+
# Formatted output
|
| 179 |
+
topk_data = []
|
| 180 |
+
for i in range(topk):
|
| 181 |
+
idx = sorted_indices[i].item()
|
| 182 |
+
token_str = tokenizer.decode([idx])
|
| 183 |
+
logit_val = sorted_logits[i].item()
|
| 184 |
+
topk_data.append({
|
| 185 |
+
"rank": i + 1,
|
| 186 |
+
"token_id": idx,
|
| 187 |
+
"token_str": token_str,
|
| 188 |
+
"logit": logit_val
|
| 189 |
+
})
|
| 190 |
+
|
| 191 |
+
# Handle Extra Tokens (if requested)
|
| 192 |
+
if extra_token_ids or extra_token_strs:
|
| 193 |
+
# Helper to find rank
|
| 194 |
+
def get_rank(val, sorted_vals):
|
| 195 |
+
# tensor search for rank
|
| 196 |
+
# val is float, sorted_vals is tensor
|
| 197 |
+
# find first index where sorted_vals < val is NOT true?
|
| 198 |
+
# sorted_vals is descending
|
| 199 |
+
# we want count of items > val
|
| 200 |
+
return (sorted_vals > val).sum().item() + 1
|
| 201 |
+
|
| 202 |
+
processed_ids = set()
|
| 203 |
+
|
| 204 |
+
# Process IDs
|
| 205 |
+
if extra_token_ids:
|
| 206 |
+
for tid in extra_token_ids:
|
| 207 |
+
if tid < 0 or tid >= len(last_logits): continue
|
| 208 |
+
if tid in processed_ids: continue
|
| 209 |
+
|
| 210 |
+
logit_val = last_logits[tid].item()
|
| 211 |
+
rank = get_rank(logit_val, sorted_logits)
|
| 212 |
+
token_str = tokenizer.decode([tid])
|
| 213 |
+
|
| 214 |
+
topk_data.append({
|
| 215 |
+
"rank": rank,
|
| 216 |
+
"token_id": tid,
|
| 217 |
+
"token_str": token_str,
|
| 218 |
+
"logit": logit_val,
|
| 219 |
+
"is_extra": True
|
| 220 |
+
})
|
| 221 |
+
processed_ids.add(tid)
|
| 222 |
+
|
| 223 |
+
# Process Strings
|
| 224 |
+
if extra_token_strs:
|
| 225 |
+
print(f"DEBUG: Processing extra strs: {extra_token_strs}")
|
| 226 |
+
for tstr in extra_token_strs:
|
| 227 |
+
# Encode
|
| 228 |
+
try:
|
| 229 |
+
# Ensure we get list of ints
|
| 230 |
+
encoded = tokenizer.encode(tstr, add_special_tokens=False)
|
| 231 |
+
print(f"DEBUG: Encoded '{tstr}' -> {encoded} (Type: {type(encoded)})")
|
| 232 |
+
|
| 233 |
+
if hasattr(encoded, 'tolist'): encoded = encoded.tolist()
|
| 234 |
+
|
| 235 |
+
if len(encoded) == 0:
|
| 236 |
+
print(f"DEBUG: Empty encoding for '{tstr}'")
|
| 237 |
+
continue
|
| 238 |
+
|
| 239 |
+
# Take first token
|
| 240 |
+
tid = encoded[0]
|
| 241 |
+
print(f"DEBUG: Using TID {tid} for '{tstr}'")
|
| 242 |
+
|
| 243 |
+
if tid in processed_ids:
|
| 244 |
+
print(f"DEBUG: TID {tid} already processed")
|
| 245 |
+
continue
|
| 246 |
+
|
| 247 |
+
logit_val = last_logits[tid].item()
|
| 248 |
+
rank = get_rank(logit_val, sorted_logits)
|
| 249 |
+
real_str = tokenizer.decode([tid])
|
| 250 |
+
|
| 251 |
+
print(f"DEBUG: Added extra token: {real_str} (ID: {tid}, Rank: {rank})")
|
| 252 |
+
|
| 253 |
+
topk_data.append({
|
| 254 |
+
"rank": rank,
|
| 255 |
+
"token_id": tid,
|
| 256 |
+
"token_str": real_str,
|
| 257 |
+
"logit": logit_val,
|
| 258 |
+
"is_extra": True
|
| 259 |
+
})
|
| 260 |
+
processed_ids.add(tid)
|
| 261 |
+
except Exception as e:
|
| 262 |
+
print(f"DEBUG: Error processing extra str '{tstr}': {e}")
|
| 263 |
+
import traceback
|
| 264 |
+
traceback.print_exc()
|
| 265 |
+
|
| 266 |
+
# Sort combined data by rank for display consistency?
|
| 267 |
+
# Or keep extras at the bottom? User request: "add more tokens in the existing top-50 table"
|
| 268 |
+
# If we sort, they mix in. If they are rank 1000, they go to bottom.
|
| 269 |
+
# But if they are rank 5 (and we showed top 10), they mix in.
|
| 270 |
+
# Let's sort.
|
| 271 |
+
topk_data.sort(key=lambda x: x['rank'])
|
| 272 |
+
|
| 273 |
+
if self.input_ids is None:
|
| 274 |
+
raise ValueError("Input IDs not found. Ensure compute_logits was run.")
|
| 275 |
+
|
| 276 |
+
# Get input tokens for visualization
|
| 277 |
+
# Robust token reconstruction ensuring spaces are preserved
|
| 278 |
+
input_tokens = []
|
| 279 |
+
# convert_ids_to_tokens usually preserves the special characters (like Ġ or )
|
| 280 |
+
raw_tokens = tokenizer.convert_ids_to_tokens(self.input_ids[0])
|
| 281 |
+
|
| 282 |
+
for t in raw_tokens:
|
| 283 |
+
# Handle bytes (common in tiktoken-based tokenizers like Qwen)
|
| 284 |
+
if isinstance(t, bytes):
|
| 285 |
+
try:
|
| 286 |
+
t = t.decode('utf-8')
|
| 287 |
+
except:
|
| 288 |
+
# Fallback for weird bytes behavior
|
| 289 |
+
t = str(t)
|
| 290 |
+
|
| 291 |
+
# If it's a string, it might still have the special whitespace characters
|
| 292 |
+
if isinstance(t, str):
|
| 293 |
+
# Replace SentencePiece underline (U+2581)
|
| 294 |
+
t = t.replace('\u2581', ' ')
|
| 295 |
+
# Replace GPT-2/RoBERTa G-dot (U+0120)
|
| 296 |
+
t = t.replace('\u0120', ' ')
|
| 297 |
+
# Replace Newline char (U+010A)
|
| 298 |
+
t = t.replace('\u010A', '\n')
|
| 299 |
+
# Replace generic replacement char just in case
|
| 300 |
+
t = t.replace('', '')
|
| 301 |
+
|
| 302 |
+
input_tokens.append(t)
|
| 303 |
+
|
| 304 |
+
return topk_data, last_logits, input_tokens
|
| 305 |
+
|
| 306 |
+
def get_target_score(self, backprop_config):
|
| 307 |
+
"""
|
| 308 |
+
Calculates the target scalar score (e.g. logit diff) based on config.
|
| 309 |
+
Returns the score tensor (attached to graph).
|
| 310 |
+
"""
|
| 311 |
+
if self.outputs is None:
|
| 312 |
+
raise ValueError("Model outputs not computed. Call compute_logits first.")
|
| 313 |
+
|
| 314 |
+
mode = backprop_config.get("mode", "max_logit")
|
| 315 |
+
last_logits = self.outputs.logits[0, -1, :]
|
| 316 |
+
sorted_logits, sorted_indices = torch.sort(last_logits, dim=-1, descending=True)
|
| 317 |
+
|
| 318 |
+
target_token_id = backprop_config.get("target_token_id")
|
| 319 |
+
if target_token_id is not None:
|
| 320 |
+
target_logit = last_logits[target_token_id]
|
| 321 |
+
else:
|
| 322 |
+
target_logit = sorted_logits[0] # Default to Top 1
|
| 323 |
+
|
| 324 |
+
if mode == "max_logit":
|
| 325 |
+
return target_logit
|
| 326 |
+
|
| 327 |
+
elif mode == "logit_diff":
|
| 328 |
+
strategy = backprop_config.get("strategy", "by_topk_avg")
|
| 329 |
+
top_logit = target_logit
|
| 330 |
+
|
| 331 |
+
if strategy == "by_ref_token":
|
| 332 |
+
ref_id = backprop_config.get("ref_token_id")
|
| 333 |
+
if ref_id is None:
|
| 334 |
+
raise ValueError("ref_token_id required for strategy 'by_ref_token'")
|
| 335 |
+
contrast_logit = last_logits[ref_id]
|
| 336 |
+
target_logit = top_logit - contrast_logit
|
| 337 |
+
|
| 338 |
+
elif strategy == "demean":
|
| 339 |
+
target_logit = top_logit - last_logits.mean()
|
| 340 |
+
|
| 341 |
+
elif strategy == "by_topk_avg":
|
| 342 |
+
k = backprop_config.get("k", 10) # default K=10
|
| 343 |
+
k = min(k, len(sorted_logits))
|
| 344 |
+
contrast_logit = sorted_logits[:k].mean()
|
| 345 |
+
target_logit = top_logit - contrast_logit
|
| 346 |
+
|
| 347 |
+
return target_logit
|
| 348 |
+
|
| 349 |
+
def run_backward_pass(self, backprop_config):
|
| 350 |
+
"""
|
| 351 |
+
Section 2 Part A: execute backward pass based on configuration.
|
| 352 |
+
"""
|
| 353 |
+
target_logit = self.get_target_score(backprop_config)
|
| 354 |
+
|
| 355 |
+
if target_logit is None:
|
| 356 |
+
raise ValueError(f"Invalid backprop configuration: {backprop_config}")
|
| 357 |
+
|
| 358 |
+
# Clear previous gradients
|
| 359 |
+
model = self.manager.get_model()
|
| 360 |
+
model.zero_grad()
|
| 361 |
+
|
| 362 |
+
# Also clear gradients on input embeddings if they exist
|
| 363 |
+
if hasattr(self, 'input_embeddings') and self.input_embeddings is not None:
|
| 364 |
+
if self.input_embeddings.grad is not None:
|
| 365 |
+
self.input_embeddings.grad.zero_()
|
| 366 |
+
|
| 367 |
+
# Run backward
|
| 368 |
+
# We need to retain grad on hidden states often?
|
| 369 |
+
# In notebook: h = outputs.hidden_states[-1]; h.retain_grad(); target_logit.backward()
|
| 370 |
+
# But we act on layer.output.grad which is captured by hook + activation
|
| 371 |
+
|
| 372 |
+
target_logit.backward(retain_graph=True) # retain_graph needed for interactive exploration where we run backward multiple times
|
| 373 |
+
|
| 374 |
+
def compute_input_attribution(self, backprop_config):
|
| 375 |
+
"""
|
| 376 |
+
Compute input attribution (Input * Gradient).
|
| 377 |
+
"""
|
| 378 |
+
# Ensure correct LRP rule is active
|
| 379 |
+
# The forward pass must have been run with the correct rule.
|
| 380 |
+
# If we detect a mismatch, we must force a reload and ask user to re-run forward.
|
| 381 |
+
# Use the currently loaded LRP rule as default (not hardcoded "Attn-LRP")
|
| 382 |
+
# to avoid false mismatch when the frontend omits lrp_rule from backprop_config.
|
| 383 |
+
default_rule = self.manager.current_lrp_rule or "Attn-LRP"
|
| 384 |
+
required_rule = backprop_config.get("lrp_rule", default_rule)
|
| 385 |
+
|
| 386 |
+
if self.manager.current_lrp_rule and self.manager.current_lrp_rule != required_rule:
|
| 387 |
+
print(f"LRP Rule Mismatch detected (Current: {self.manager.current_lrp_rule}, Requested: {required_rule})")
|
| 388 |
+
print(f"Reloading model {self.manager.current_model_path} with rule={required_rule}...")
|
| 389 |
+
|
| 390 |
+
old_rule = self.manager.current_lrp_rule
|
| 391 |
+
|
| 392 |
+
self.manager.load_model(
|
| 393 |
+
model_path=self.manager.current_model_path,
|
| 394 |
+
dtype=self.manager.current_dtype,
|
| 395 |
+
lrp_rule=required_rule
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
# Since the forward pass graph (self.outputs) was built with the OLD rule,
|
| 399 |
+
# we cannot proceed. The user must re-run compute_logits.
|
| 400 |
+
raise RuntimeError(
|
| 401 |
+
f"LRP rule changed from '{old_rule}' to '{required_rule}'. "
|
| 402 |
+
"The model has been reloaded. You MUST re-run 'compute_logits()' to rebuild the computation graph with the new rule, "
|
| 403 |
+
"then call 'compute_input_attribution()' again."
|
| 404 |
+
)
|
| 405 |
+
# backprop_config['target_token_id'] = 2877
|
| 406 |
+
self.run_backward_pass(backprop_config)
|
| 407 |
+
|
| 408 |
+
# Calculate relevance: (input * grad).sum(-1)
|
| 409 |
+
# self.input_embeddings is [Batch, Seq, Dim]
|
| 410 |
+
if self.input_embeddings.grad is None:
|
| 411 |
+
raise RuntimeError("No gradient found on input embeddings. Ensure compute_logits was run.")
|
| 412 |
+
|
| 413 |
+
relevance = (self.input_embeddings * self.input_embeddings.grad).float().sum(-1).detach().cpu()[0]
|
| 414 |
+
print(f"Computed input attribution with shape: {relevance}")
|
| 415 |
+
# Return raw relevance
|
| 416 |
+
return relevance.tolist()
|
| 417 |
+
|
| 418 |
+
def compute_input_attribution_gradient(self, backprop_config):
|
| 419 |
+
"""
|
| 420 |
+
Compute input attribution using vanilla gradient method (Input * Gradient).
|
| 421 |
+
This does NOT require LRP monkey-patching - uses standard PyTorch autograd.
|
| 422 |
+
The gradient flows through normal attention/MLP without LRP decomposition rules.
|
| 423 |
+
"""
|
| 424 |
+
if self.outputs is None:
|
| 425 |
+
raise RuntimeError("No forward pass found. Run compute_logits first.")
|
| 426 |
+
|
| 427 |
+
# Run backward pass (works on vanilla model without LRP)
|
| 428 |
+
self.run_backward_pass(backprop_config)
|
| 429 |
+
|
| 430 |
+
# Calculate relevance: (input * grad).sum(-1)
|
| 431 |
+
if self.input_embeddings.grad is None:
|
| 432 |
+
raise RuntimeError("No gradient found on input embeddings. Ensure compute_logits was run.")
|
| 433 |
+
|
| 434 |
+
relevance = (self.input_embeddings * self.input_embeddings.grad).float().sum(-1).detach().cpu()[0]
|
| 435 |
+
print(f"Computed GRADIENT input attribution with shape: {relevance.shape}")
|
| 436 |
+
return relevance.tolist()
|
| 437 |
+
|
| 438 |
+
def compute_perturbation_eval(self, attribution_scores, k_values, target_token_id):
|
| 439 |
+
"""
|
| 440 |
+
Evaluate attribution quality by perturbing top-attributed tokens.
|
| 441 |
+
|
| 442 |
+
For each k in k_values:
|
| 443 |
+
1. Sort tokens by |attribution score| descending
|
| 444 |
+
2. Take top-k token indices
|
| 445 |
+
3. Clone input embeddings, zero out those k tokens' embeddings
|
| 446 |
+
4. Run forward pass with perturbed embeddings
|
| 447 |
+
5. Check if the error token (target_token_id) is still top-1
|
| 448 |
+
|
| 449 |
+
Args:
|
| 450 |
+
attribution_scores: list of floats (one per input token)
|
| 451 |
+
k_values: list of int (e.g., [1, 3, 5, 10])
|
| 452 |
+
target_token_id: int - the error token ID to check
|
| 453 |
+
|
| 454 |
+
Returns:
|
| 455 |
+
list of result dicts for each k
|
| 456 |
+
"""
|
| 457 |
+
if self.input_ids is None or self.input_embeddings is None:
|
| 458 |
+
raise RuntimeError("No forward pass found. Run compute_logits first.")
|
| 459 |
+
|
| 460 |
+
model = self.manager.get_model()
|
| 461 |
+
tokenizer = self.manager.get_tokenizer()
|
| 462 |
+
device = model.device
|
| 463 |
+
|
| 464 |
+
# Get original top-1 prediction for reference
|
| 465 |
+
with torch.no_grad():
|
| 466 |
+
original_logits = model(
|
| 467 |
+
inputs_embeds=self.input_embeddings.detach(),
|
| 468 |
+
use_cache=False
|
| 469 |
+
).logits[0, -1, :]
|
| 470 |
+
original_top1_id = original_logits.argmax().item()
|
| 471 |
+
original_target_logit = original_logits[target_token_id].item()
|
| 472 |
+
|
| 473 |
+
# Sort tokens by |attribution score| descending
|
| 474 |
+
scores = torch.tensor(attribution_scores, dtype=torch.float32)
|
| 475 |
+
sorted_indices = torch.argsort(scores.abs(), descending=True)
|
| 476 |
+
|
| 477 |
+
seq_len = self.input_embeddings.shape[1]
|
| 478 |
+
|
| 479 |
+
results = []
|
| 480 |
+
for k in k_values:
|
| 481 |
+
k_clamped = min(k, seq_len)
|
| 482 |
+
top_k_indices = sorted_indices[:k_clamped].tolist()
|
| 483 |
+
|
| 484 |
+
# Get the token strings being perturbed
|
| 485 |
+
perturbed_token_strs = []
|
| 486 |
+
for idx in top_k_indices:
|
| 487 |
+
if idx < len(self.input_ids[0]):
|
| 488 |
+
tid = self.input_ids[0][idx].item()
|
| 489 |
+
perturbed_token_strs.append(tokenizer.decode([tid]))
|
| 490 |
+
else:
|
| 491 |
+
perturbed_token_strs.append("?")
|
| 492 |
+
|
| 493 |
+
# Clone embeddings and zero out top-k tokens
|
| 494 |
+
perturbed_embeddings = self.input_embeddings.detach().clone()
|
| 495 |
+
for idx in top_k_indices:
|
| 496 |
+
perturbed_embeddings[0, idx, :] = 0.0
|
| 497 |
+
|
| 498 |
+
# Forward pass with perturbed embeddings
|
| 499 |
+
with torch.no_grad():
|
| 500 |
+
perturbed_logits = model(
|
| 501 |
+
inputs_embeds=perturbed_embeddings,
|
| 502 |
+
use_cache=False
|
| 503 |
+
).logits[0, -1, :]
|
| 504 |
+
|
| 505 |
+
perturbed_top1_id = perturbed_logits.argmax().item()
|
| 506 |
+
perturbed_top1_str = tokenizer.decode([perturbed_top1_id])
|
| 507 |
+
perturbed_target_logit = perturbed_logits[target_token_id].item()
|
| 508 |
+
|
| 509 |
+
# Error is "fixed" if the target token is no longer top-1
|
| 510 |
+
error_fixed = (perturbed_top1_id != target_token_id)
|
| 511 |
+
|
| 512 |
+
# Compute logit change
|
| 513 |
+
logit_change = perturbed_target_logit - original_target_logit
|
| 514 |
+
|
| 515 |
+
# Compute rank of target token after perturbation
|
| 516 |
+
sorted_perturbed, sorted_perturbed_idx = torch.sort(perturbed_logits, descending=True)
|
| 517 |
+
target_rank_after = (sorted_perturbed_idx == target_token_id).nonzero(as_tuple=True)[0].item() + 1
|
| 518 |
+
|
| 519 |
+
results.append({
|
| 520 |
+
"k": k,
|
| 521 |
+
"perturbed_tokens": perturbed_token_strs,
|
| 522 |
+
"perturbed_indices": top_k_indices,
|
| 523 |
+
"new_top1_token_id": perturbed_top1_id,
|
| 524 |
+
"new_top1_token_str": perturbed_top1_str,
|
| 525 |
+
"error_fixed": error_fixed,
|
| 526 |
+
"original_target_logit": round(original_target_logit, 4),
|
| 527 |
+
"perturbed_target_logit": round(perturbed_target_logit, 4),
|
| 528 |
+
"logit_change": round(logit_change, 4),
|
| 529 |
+
"target_rank_after": target_rank_after
|
| 530 |
+
})
|
| 531 |
+
|
| 532 |
+
print(f"Perturbation k={k}: error_fixed={error_fixed}, "
|
| 533 |
+
f"new_top1='{perturbed_top1_str}' (ID={perturbed_top1_id}), "
|
| 534 |
+
f"logit_change={logit_change:.4f}, target_rank={target_rank_after}")
|
| 535 |
+
|
| 536 |
+
del perturbed_embeddings
|
| 537 |
+
|
| 538 |
+
torch.cuda.empty_cache()
|
| 539 |
+
return results
|
| 540 |
+
|
| 541 |
+
def compute_perturbation_manual(self, perturb_indices, target_token_id):
|
| 542 |
+
"""
|
| 543 |
+
Evaluate attribution by perturbing manually selected token positions.
|
| 544 |
+
|
| 545 |
+
Args:
|
| 546 |
+
perturb_indices: list of int - token position indices to zero out
|
| 547 |
+
target_token_id: int - the error token ID to check
|
| 548 |
+
|
| 549 |
+
Returns:
|
| 550 |
+
dict with perturbation result
|
| 551 |
+
"""
|
| 552 |
+
if self.input_ids is None or self.input_embeddings is None:
|
| 553 |
+
raise RuntimeError("No forward pass found. Run compute_logits first.")
|
| 554 |
+
|
| 555 |
+
model = self.manager.get_model()
|
| 556 |
+
tokenizer = self.manager.get_tokenizer()
|
| 557 |
+
seq_len = self.input_embeddings.shape[1]
|
| 558 |
+
|
| 559 |
+
# Validate indices
|
| 560 |
+
valid_indices = [idx for idx in perturb_indices if 0 <= idx < seq_len]
|
| 561 |
+
if len(valid_indices) == 0:
|
| 562 |
+
raise ValueError("No valid token indices provided.")
|
| 563 |
+
|
| 564 |
+
# Get original top-1 prediction for reference
|
| 565 |
+
with torch.no_grad():
|
| 566 |
+
original_logits = model(
|
| 567 |
+
inputs_embeds=self.input_embeddings.detach(),
|
| 568 |
+
use_cache=False
|
| 569 |
+
).logits[0, -1, :]
|
| 570 |
+
original_top1_id = original_logits.argmax().item()
|
| 571 |
+
original_target_logit = original_logits[target_token_id].item()
|
| 572 |
+
|
| 573 |
+
# Get the token strings being perturbed
|
| 574 |
+
perturbed_token_strs = []
|
| 575 |
+
for idx in valid_indices:
|
| 576 |
+
if idx < len(self.input_ids[0]):
|
| 577 |
+
tid = self.input_ids[0][idx].item()
|
| 578 |
+
perturbed_token_strs.append(tokenizer.decode([tid]))
|
| 579 |
+
else:
|
| 580 |
+
perturbed_token_strs.append("?")
|
| 581 |
+
|
| 582 |
+
# Clone embeddings and zero out selected tokens
|
| 583 |
+
perturbed_embeddings = self.input_embeddings.detach().clone()
|
| 584 |
+
for idx in valid_indices:
|
| 585 |
+
perturbed_embeddings[0, idx, :] = 0.0
|
| 586 |
+
|
| 587 |
+
# Forward pass with perturbed embeddings
|
| 588 |
+
with torch.no_grad():
|
| 589 |
+
perturbed_logits = model(
|
| 590 |
+
inputs_embeds=perturbed_embeddings,
|
| 591 |
+
use_cache=False
|
| 592 |
+
).logits[0, -1, :]
|
| 593 |
+
|
| 594 |
+
perturbed_top1_id = perturbed_logits.argmax().item()
|
| 595 |
+
perturbed_top1_str = tokenizer.decode([perturbed_top1_id])
|
| 596 |
+
perturbed_target_logit = perturbed_logits[target_token_id].item()
|
| 597 |
+
|
| 598 |
+
# Error is "fixed" if the target token is no longer top-1
|
| 599 |
+
error_fixed = (perturbed_top1_id != target_token_id)
|
| 600 |
+
|
| 601 |
+
# Compute logit change
|
| 602 |
+
logit_change = perturbed_target_logit - original_target_logit
|
| 603 |
+
|
| 604 |
+
# Compute rank of target token after perturbation
|
| 605 |
+
sorted_perturbed, sorted_perturbed_idx = torch.sort(perturbed_logits, descending=True)
|
| 606 |
+
target_rank_after = (sorted_perturbed_idx == target_token_id).nonzero(as_tuple=True)[0].item() + 1
|
| 607 |
+
|
| 608 |
+
result = {
|
| 609 |
+
"k": len(valid_indices),
|
| 610 |
+
"perturbed_tokens": perturbed_token_strs,
|
| 611 |
+
"perturbed_indices": valid_indices,
|
| 612 |
+
"new_top1_token_id": perturbed_top1_id,
|
| 613 |
+
"new_top1_token_str": perturbed_top1_str,
|
| 614 |
+
"error_fixed": error_fixed,
|
| 615 |
+
"original_target_logit": round(original_target_logit, 4),
|
| 616 |
+
"perturbed_target_logit": round(perturbed_target_logit, 4),
|
| 617 |
+
"logit_change": round(logit_change, 4),
|
| 618 |
+
"target_rank_after": target_rank_after
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
print(f"Manual Perturbation ({len(valid_indices)} tokens): error_fixed={error_fixed}, "
|
| 622 |
+
f"new_top1='{perturbed_top1_str}' (ID={perturbed_top1_id}), "
|
| 623 |
+
f"logit_change={logit_change:.4f}, target_rank={target_rank_after}")
|
| 624 |
+
|
| 625 |
+
del perturbed_embeddings
|
| 626 |
+
torch.cuda.empty_cache()
|
| 627 |
+
return result
|
| 628 |
+
|
| 629 |
+
def compute_connection_matrix_gen(self, source, target, node_threshold=None):
|
| 630 |
+
"""
|
| 631 |
+
Section 2 Part B: Compute Token-to-Token interaction matrix between two nodes.
|
| 632 |
+
Generator version that yields progress.
|
| 633 |
+
source, target: int (layer idx) or tuple (layer_idx, 'mid'/'post')
|
| 634 |
+
"""
|
| 635 |
+
source_layer_idx, source_type = self._parse_node(source)
|
| 636 |
+
target_layer_idx, target_type = self._parse_node(target)
|
| 637 |
+
|
| 638 |
+
model = self.manager.get_model()
|
| 639 |
+
layers = model.model.layers
|
| 640 |
+
|
| 641 |
+
target_layer = layers[target_layer_idx]
|
| 642 |
+
|
| 643 |
+
# 1. Identify Source Tensor
|
| 644 |
+
if source_layer_idx == -1:
|
| 645 |
+
source_tensor = self.input_embeddings
|
| 646 |
+
else:
|
| 647 |
+
layer = layers[source_layer_idx]
|
| 648 |
+
if source_type == 'mid':
|
| 649 |
+
# Use Decomposer to get module
|
| 650 |
+
mid_mod = self.manager.decomposer.get_mid_activation_module(layer)
|
| 651 |
+
if not mid_mod:
|
| 652 |
+
raise ValueError(f"Decomposer could not identify mid-activation module for layer {source_layer_idx}")
|
| 653 |
+
|
| 654 |
+
source_tensor = getattr(mid_mod, 'mid_activation', None)
|
| 655 |
+
if source_tensor is None:
|
| 656 |
+
raise ValueError(f"Mid activation for layer {source_layer_idx} not captured. Enable capture_mid in compute_logits.")
|
| 657 |
+
else:
|
| 658 |
+
source_tensor = layer.output
|
| 659 |
+
|
| 660 |
+
# 2. Identify Target Tensor and Gradient
|
| 661 |
+
if target_type == 'mid':
|
| 662 |
+
# We need the gradient at the mid point (input to post_attn_norm)
|
| 663 |
+
mid_mod = self.manager.decomposer.get_mid_activation_module(target_layer)
|
| 664 |
+
if not mid_mod:
|
| 665 |
+
raise ValueError(f"Decomposer could not identify mid-activation module for target {target_layer_idx}")
|
| 666 |
+
|
| 667 |
+
target_tensor = getattr(mid_mod, 'mid_activation', None)
|
| 668 |
+
if target_tensor is None:
|
| 669 |
+
raise ValueError(f"Mid activation for target {target_layer_idx} not captured.")
|
| 670 |
+
else:
|
| 671 |
+
target_tensor = target_layer.output
|
| 672 |
+
|
| 673 |
+
target_grad = target_tensor.grad
|
| 674 |
+
|
| 675 |
+
# Disable gradient checkpointing temporarily
|
| 676 |
+
was_checkpointing = model.is_gradient_checkpointing
|
| 677 |
+
if was_checkpointing:
|
| 678 |
+
model.gradient_checkpointing_disable()
|
| 679 |
+
|
| 680 |
+
try:
|
| 681 |
+
# Prepare Input
|
| 682 |
+
target_layer_input = source_tensor.detach()
|
| 683 |
+
batch_size, seq_len, hidden_dim = target_layer_input.shape
|
| 684 |
+
|
| 685 |
+
# Target Real Relevance
|
| 686 |
+
if target_grad is not None:
|
| 687 |
+
real_target_rel = (target_tensor * target_grad).sum(dim=-1)[0]
|
| 688 |
+
else:
|
| 689 |
+
real_target_rel = torch.zeros(seq_len, device=model.device)
|
| 690 |
+
|
| 691 |
+
# Filter Indices
|
| 692 |
+
total_params = model.num_parameters()
|
| 693 |
+
if node_threshold is None: node_threshold = 0.01
|
| 694 |
+
|
| 695 |
+
if node_threshold > 0:
|
| 696 |
+
indices_to_compute = torch.nonzero(real_target_rel.abs() > node_threshold).squeeze(-1).tolist()
|
| 697 |
+
if isinstance(indices_to_compute, int): indices_to_compute = [indices_to_compute]
|
| 698 |
+
print(f"DEBUG: Node Threshold {node_threshold}. Computing for {len(indices_to_compute)}/{seq_len} nodes.")
|
| 699 |
+
else:
|
| 700 |
+
indices_to_compute = list(range(seq_len))
|
| 701 |
+
|
| 702 |
+
# Fixed Position IDs (for Rotary)
|
| 703 |
+
position_ids = torch.arange(0, seq_len, dtype=torch.long, device=model.device).unsqueeze(0)
|
| 704 |
+
|
| 705 |
+
# Construct Operation Sequence
|
| 706 |
+
ops = []
|
| 707 |
+
if source_layer_idx != -1:
|
| 708 |
+
if source_type == 'mid':
|
| 709 |
+
ops.append(('part2', layers[source_layer_idx]))
|
| 710 |
+
|
| 711 |
+
# Intermediate Layers
|
| 712 |
+
for i in range(source_layer_idx + 1, target_layer_idx):
|
| 713 |
+
ops.append(('part1', layers[i]))
|
| 714 |
+
ops.append(('part2', layers[i]))
|
| 715 |
+
|
| 716 |
+
# Target Layer
|
| 717 |
+
if target_layer_idx > source_layer_idx:
|
| 718 |
+
ops.append(('part1', layers[target_layer_idx]))
|
| 719 |
+
if target_type == 'post':
|
| 720 |
+
ops.append(('part2', layers[target_layer_idx]))
|
| 721 |
+
elif target_layer_idx == source_layer_idx:
|
| 722 |
+
pass # Already handled or identity
|
| 723 |
+
elif source_layer_idx == -1:
|
| 724 |
+
# Special case: source is embeddings, target is 0
|
| 725 |
+
# Range was (0,0) empty.
|
| 726 |
+
# Need to add target 0 parts
|
| 727 |
+
ops.append(('part1', layers[target_layer_idx]))
|
| 728 |
+
if target_type == 'post':
|
| 729 |
+
ops.append(('part2', layers[target_layer_idx]))
|
| 730 |
+
|
| 731 |
+
# Pre-calc Rotary Embedding (using dummy execution or helper)
|
| 732 |
+
# We assume rotary depends only on position_ids and shape
|
| 733 |
+
rotary_emb = None
|
| 734 |
+
if hasattr(model.model, 'rotary_emb'):
|
| 735 |
+
rotary_emb = model.model.rotary_emb(target_layer_input, position_ids)
|
| 736 |
+
elif hasattr(model.model, 'rotary_embs') and 'full_attention' in model.model.rotary_embs:
|
| 737 |
+
rotary_emb = model.model.rotary_embs['full_attention'](target_layer_input, position_ids)
|
| 738 |
+
elif hasattr(model.model, 'rotary_embs') and len(model.model.rotary_embs) > 0:
|
| 739 |
+
rotary_emb = list(model.model.rotary_embs.values())[0](target_layer_input, position_ids)
|
| 740 |
+
|
| 741 |
+
# Chunk Processing
|
| 742 |
+
current_dtype = target_layer_input.dtype
|
| 743 |
+
BATCH_CHUNK_SIZE = get_batch_chunk_size(total_params, current_dtype)
|
| 744 |
+
token_interaction = torch.zeros(seq_len, seq_len, device=model.device)
|
| 745 |
+
target_grad_full = target_grad # Alias
|
| 746 |
+
|
| 747 |
+
total_items = len(indices_to_compute)
|
| 748 |
+
print(f"DEBUG: BATCH_CHUNK_SIZE={BATCH_CHUNK_SIZE}, total_items={total_items}, seq_len={seq_len}, params={total_params/1e9:.2f}B, dtype={current_dtype}")
|
| 749 |
+
|
| 750 |
+
for i in range(0, total_items, BATCH_CHUNK_SIZE):
|
| 751 |
+
yield {"type": "progress", "current": i, "total": total_items}
|
| 752 |
+
|
| 753 |
+
chunk_indices = indices_to_compute[i : i + BATCH_CHUNK_SIZE]
|
| 754 |
+
current_batch_size = len(chunk_indices)
|
| 755 |
+
|
| 756 |
+
expanded_input = target_layer_input.expand(current_batch_size, seq_len, hidden_dim).clone().requires_grad_(True)
|
| 757 |
+
|
| 758 |
+
# Execute Ops
|
| 759 |
+
hidden_states = expanded_input
|
| 760 |
+
for op_type, layer_mod in ops:
|
| 761 |
+
if op_type == 'part1':
|
| 762 |
+
hidden_states = self._forward_part1(layer_mod, hidden_states, position_embeddings=rotary_emb)
|
| 763 |
+
else:
|
| 764 |
+
hidden_states = self._forward_part2(layer_mod, hidden_states)
|
| 765 |
+
|
| 766 |
+
reconstructed_output = hidden_states
|
| 767 |
+
|
| 768 |
+
# Backward
|
| 769 |
+
grad_output_chunk = torch.zeros(current_batch_size, seq_len, hidden_dim, dtype=reconstructed_output.dtype, device=model.device)
|
| 770 |
+
|
| 771 |
+
for batch_idx, global_idx in enumerate(chunk_indices):
|
| 772 |
+
if target_grad_full is not None:
|
| 773 |
+
grad_output_chunk[batch_idx, global_idx, :] = target_grad_full[0, global_idx, :]
|
| 774 |
+
|
| 775 |
+
grad_input = torch.autograd.grad(outputs=reconstructed_output, inputs=expanded_input, grad_outputs=grad_output_chunk, retain_graph=False)[0]
|
| 776 |
+
|
| 777 |
+
chunk_relevance = (grad_input * expanded_input).sum(dim=-1)
|
| 778 |
+
token_interaction[chunk_indices, :] = chunk_relevance.detach().to(token_interaction.dtype)
|
| 779 |
+
|
| 780 |
+
del expanded_input, hidden_states, reconstructed_output, grad_output_chunk, grad_input, chunk_relevance
|
| 781 |
+
torch.cuda.empty_cache()
|
| 782 |
+
|
| 783 |
+
# Source Real Relevance
|
| 784 |
+
if source_layer_idx == -1:
|
| 785 |
+
if self.input_embeddings.grad is not None:
|
| 786 |
+
real_source_rel = (self.input_embeddings * self.input_embeddings.grad).sum(dim=-1)[0]
|
| 787 |
+
else:
|
| 788 |
+
real_source_rel = torch.zeros(seq_len, device=model.device)
|
| 789 |
+
else:
|
| 790 |
+
if source_tensor.grad is not None:
|
| 791 |
+
real_source_rel = (source_tensor * source_tensor.grad).sum(dim=-1)[0]
|
| 792 |
+
else:
|
| 793 |
+
real_source_rel = torch.zeros(seq_len, device=model.device)
|
| 794 |
+
|
| 795 |
+
yield {
|
| 796 |
+
"type": "result",
|
| 797 |
+
"payload": {
|
| 798 |
+
"matrix": token_interaction.detach().float().cpu().numpy(),
|
| 799 |
+
"real_target_rel": real_target_rel.detach().float().cpu().numpy(),
|
| 800 |
+
"real_source_rel": real_source_rel.detach().float().cpu().numpy()
|
| 801 |
+
}
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
finally:
|
| 805 |
+
if was_checkpointing:
|
| 806 |
+
model.gradient_checkpointing_enable()
|
| 807 |
+
|
| 808 |
+
def compute_connection_matrix(self, source, target):
|
| 809 |
+
for item in self.compute_connection_matrix_gen(source, target):
|
| 810 |
+
if item.get("type") == "result":
|
| 811 |
+
return item["payload"]
|
| 812 |
+
return None
|
backend/error_token_location.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
logging.basicConfig(level=logging.INFO,
|
| 3 |
+
format='%(asctime)s | %(levelname)-6s | %(name)-40s || %(message)s',
|
| 4 |
+
datefmt='%m-%d %H:%M:%S')
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import yaml
|
| 12 |
+
import json
|
| 13 |
+
from typing import Optional
|
| 14 |
+
from jinja2 import Template
|
| 15 |
+
|
| 16 |
+
# Use env-var based OpenAI client instead of Azure-specific endpoint
|
| 17 |
+
_openai_client = None
|
| 18 |
+
|
| 19 |
+
def _get_openai_client():
|
| 20 |
+
"""Get or create OpenAI client using environment variables."""
|
| 21 |
+
global _openai_client
|
| 22 |
+
if _openai_client is not None:
|
| 23 |
+
return _openai_client
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
import openai
|
| 27 |
+
except ImportError:
|
| 28 |
+
logger.warning("openai package not installed. LLM-based error token localization will not be available.")
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
api_key = os.environ.get("OPENAI_API_KEY")
|
| 32 |
+
if not api_key:
|
| 33 |
+
logger.warning("OPENAI_API_KEY not set. LLM-based error token localization will not be available.")
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
base_url = os.environ.get("OPENAI_BASE_URL", None)
|
| 37 |
+
kwargs = {"api_key": api_key}
|
| 38 |
+
if base_url:
|
| 39 |
+
kwargs["base_url"] = base_url
|
| 40 |
+
|
| 41 |
+
_openai_client = openai.OpenAI(**kwargs)
|
| 42 |
+
return _openai_client
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _get_default_models():
|
| 46 |
+
"""Get the list of LLM models to use as validators from env or defaults."""
|
| 47 |
+
models_str = os.environ.get("LLM_MODELS", "gpt-4o-mini")
|
| 48 |
+
return [m.strip() for m in models_str.split(",") if m.strip()]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class ErrorTokenLocator:
|
| 52 |
+
def __init__(self, model, tokenizer, prompt_template_yaml=None):
|
| 53 |
+
"""
|
| 54 |
+
Initialize the error token locator
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
model: The language model to use
|
| 58 |
+
tokenizer: The corresponding tokenizer for tokenizing text
|
| 59 |
+
prompt_template_yaml (str, optional): Path to the prompt template YAML file, uses default template when None
|
| 60 |
+
"""
|
| 61 |
+
self.model = model
|
| 62 |
+
self.tokenizer = tokenizer
|
| 63 |
+
|
| 64 |
+
self.client = _get_openai_client()
|
| 65 |
+
|
| 66 |
+
self.endpoint_list = _get_default_models()
|
| 67 |
+
|
| 68 |
+
if prompt_template_yaml is None:
|
| 69 |
+
self.system_prompt = self.load_general_prompt_template()
|
| 70 |
+
else:
|
| 71 |
+
self.system_prompt = self.load_general_prompt_template(prompt_template_yaml)
|
| 72 |
+
|
| 73 |
+
def prompt_constructor(self, query, completion, ground_truth: str=None):
|
| 74 |
+
"""
|
| 75 |
+
Construct prompts for error token location
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
query (str): The input query/prompt
|
| 79 |
+
completion (str): The completion text generated by the model
|
| 80 |
+
ground_truth (str, optional): The correct answer/ground truth, defaults to None
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
tuple: (msg, tokens)
|
| 84 |
+
- msg (list): The constructed conversation message list containing system and user messages
|
| 85 |
+
- tokens (list): List of token IDs for the completion
|
| 86 |
+
"""
|
| 87 |
+
tokens = self.tokenizer(completion).input_ids
|
| 88 |
+
indexed_completion = ""
|
| 89 |
+
for i, tok in enumerate(tokens):
|
| 90 |
+
indexed_completion += f"{self.tokenizer.decode([tok])}[{i}] "
|
| 91 |
+
|
| 92 |
+
user_msg_content = {
|
| 93 |
+
"prompt": query,
|
| 94 |
+
"completion": completion,
|
| 95 |
+
"indexed_completion": indexed_completion,
|
| 96 |
+
"ground_truth": ground_truth
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
msg = [
|
| 100 |
+
{"role": "system", "content": self.system_prompt},
|
| 101 |
+
{"role": "user", "content": json.dumps(user_msg_content, indent=2)}
|
| 102 |
+
]
|
| 103 |
+
return msg, tokens
|
| 104 |
+
|
| 105 |
+
def load_general_prompt_template(self, prompt_template_yaml=None):
|
| 106 |
+
"""
|
| 107 |
+
Load the general prompt template
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
prompt_template_yaml (str, optional): Path to the YAML template file, uses default path when None
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
str: The rendered system prompt template string
|
| 114 |
+
"""
|
| 115 |
+
if prompt_template_yaml is None:
|
| 116 |
+
# Use path relative to this file's location
|
| 117 |
+
prompt_template_yaml = os.path.join(
|
| 118 |
+
os.path.dirname(os.path.abspath(__file__)),
|
| 119 |
+
"token_locator_prompts",
|
| 120 |
+
"err_token_localization.yaml"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
with open(prompt_template_yaml, "r", encoding='utf-8') as f:
|
| 124 |
+
system_prompt_temp = yaml.safe_load(f)
|
| 125 |
+
system_prompt = Template(system_prompt_temp['system']).render(
|
| 126 |
+
dataset_description="No dataset description provided.",
|
| 127 |
+
dataset_specific_instructions="-No dataset specific instructions provided."
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
return system_prompt
|
| 131 |
+
|
| 132 |
+
def call_validator(self, msg, tokens, endpoint_list=None):
|
| 133 |
+
"""
|
| 134 |
+
Call the validator for error token location validation
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
msg (list): The constructed conversation message list
|
| 138 |
+
tokens (list): List of token IDs
|
| 139 |
+
endpoint_list (list): List of validator endpoint names
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
tuple: (completion_before_err, explanation, vote_details)
|
| 143 |
+
- completion_before_err (str): Completion text truncated before the error token
|
| 144 |
+
- explanation (str): Explanation of why this token is the error
|
| 145 |
+
- vote_details (dict): Detailed voting information for each validator
|
| 146 |
+
"""
|
| 147 |
+
if self.client is None:
|
| 148 |
+
raise RuntimeError(
|
| 149 |
+
"OpenAI client not available. Please set OPENAI_API_KEY environment variable "
|
| 150 |
+
"or use manual truncation instead."
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
responses = []
|
| 154 |
+
if endpoint_list is None:
|
| 155 |
+
endpoint_list = self.endpoint_list
|
| 156 |
+
for model_name in endpoint_list:
|
| 157 |
+
response = self.client.chat.completions.create(
|
| 158 |
+
model=model_name,
|
| 159 |
+
messages=msg,
|
| 160 |
+
temperature=0,
|
| 161 |
+
seed=42,
|
| 162 |
+
top_p=1,
|
| 163 |
+
frequency_penalty=0,
|
| 164 |
+
presence_penalty=0,
|
| 165 |
+
response_format={"type": "json_object"}
|
| 166 |
+
)
|
| 167 |
+
responses.append(response)
|
| 168 |
+
|
| 169 |
+
votes = {}
|
| 170 |
+
first_response_for_token = {}
|
| 171 |
+
validator_votes = {} # Track each validator's vote
|
| 172 |
+
|
| 173 |
+
for idx, response in enumerate(responses):
|
| 174 |
+
model_name = endpoint_list[idx]
|
| 175 |
+
try:
|
| 176 |
+
res_json = json.loads(response.choices[0].message.content)
|
| 177 |
+
token_index = int(res_json["token_index"])
|
| 178 |
+
explanation = res_json.get("explanation", "")
|
| 179 |
+
|
| 180 |
+
# Track votes
|
| 181 |
+
votes[token_index] = votes.get(token_index, 0) + 1
|
| 182 |
+
if token_index not in first_response_for_token:
|
| 183 |
+
first_response_for_token[token_index] = (response, idx)
|
| 184 |
+
|
| 185 |
+
# Store each validator's vote details
|
| 186 |
+
validator_votes[model_name] = {
|
| 187 |
+
"token_index": token_index,
|
| 188 |
+
"error_token": self.tokenizer.decode([tokens[token_index]]) if token_index < len(tokens) else "N/A",
|
| 189 |
+
"explanation": explanation
|
| 190 |
+
}
|
| 191 |
+
except Exception as e:
|
| 192 |
+
logger.error(f"Error processing response from {model_name}: {e}")
|
| 193 |
+
validator_votes[model_name] = {
|
| 194 |
+
"token_index": -1,
|
| 195 |
+
"error_token": "Error",
|
| 196 |
+
"explanation": f"Failed to parse response: {str(e)}"
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
if not votes:
|
| 200 |
+
return responses[0] if responses else None, "", {}
|
| 201 |
+
|
| 202 |
+
max_votes = max(votes.values())
|
| 203 |
+
candidates = [t for t, c in votes.items() if c == max_votes]
|
| 204 |
+
# pick the candidate whose first corresponding response appeared earliest
|
| 205 |
+
winner_token = min(candidates, key=lambda t: first_response_for_token[t][1])
|
| 206 |
+
|
| 207 |
+
response = first_response_for_token[winner_token][0].choices[0].message.content
|
| 208 |
+
failure_rca = json.loads(response)
|
| 209 |
+
completion_before_err = ""
|
| 210 |
+
for idx, tok in enumerate(tokens):
|
| 211 |
+
if idx == failure_rca["token_index"]:
|
| 212 |
+
break
|
| 213 |
+
completion_before_err += f"{self.tokenizer.decode([tok])}"
|
| 214 |
+
explanation = failure_rca["explanation"]
|
| 215 |
+
|
| 216 |
+
# Add vote summary to vote_details
|
| 217 |
+
vote_summary = {
|
| 218 |
+
"winner_token_index": winner_token,
|
| 219 |
+
"winner_votes": max_votes,
|
| 220 |
+
"total_validators": len(endpoint_list),
|
| 221 |
+
"vote_distribution": votes
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
return completion_before_err, explanation, {
|
| 225 |
+
"validators": validator_votes,
|
| 226 |
+
"summary": vote_summary
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
def locate_error_token(self, prompt: str, completion: str, ground_truth: str = None,
|
| 230 |
+
validators: Optional[list] = None,
|
| 231 |
+
use_llm: bool = True,
|
| 232 |
+
manual_chunks: Optional[list] = None):
|
| 233 |
+
"""
|
| 234 |
+
Main method to locate the error token in a completion
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
prompt (str): The input prompt
|
| 238 |
+
completion (str): The completion text to analyze
|
| 239 |
+
ground_truth (str, optional): The correct answer/ground truth, defaults to None
|
| 240 |
+
|
| 241 |
+
Returns:
|
| 242 |
+
dict: Dictionary containing:
|
| 243 |
+
- status (str): "success" or "error"
|
| 244 |
+
- truncated_text (str): Prompt + completion truncated before error token
|
| 245 |
+
- explanation (str): Explanation of the error
|
| 246 |
+
- error_token_index (int): Index of the error token
|
| 247 |
+
"""
|
| 248 |
+
try:
|
| 249 |
+
# Construct prompt messages
|
| 250 |
+
msg, tokens = self.prompt_constructor(prompt, completion, ground_truth)
|
| 251 |
+
|
| 252 |
+
# If user requests to skip LLM search, use manual chunks if provided
|
| 253 |
+
if not use_llm:
|
| 254 |
+
if manual_chunks and len(manual_chunks) > 0:
|
| 255 |
+
manual_chunk = manual_chunks[0]
|
| 256 |
+
completion_before_err = manual_chunk
|
| 257 |
+
explanation = "Manual chunk provided by user (LLM search skipped)."
|
| 258 |
+
error_token_index = len(self.tokenizer(manual_chunk).input_ids)
|
| 259 |
+
truncated_text = prompt + completion_before_err
|
| 260 |
+
return {
|
| 261 |
+
"status": "success",
|
| 262 |
+
"truncated_text": truncated_text,
|
| 263 |
+
"explanation": explanation,
|
| 264 |
+
"error_token_index": error_token_index
|
| 265 |
+
}
|
| 266 |
+
else:
|
| 267 |
+
return {
|
| 268 |
+
"status": "error",
|
| 269 |
+
"message": "LLM search disabled but no manual chunk provided.",
|
| 270 |
+
"truncated_text": "",
|
| 271 |
+
"explanation": ""
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
# Check if OpenAI client is available
|
| 275 |
+
if self.client is None:
|
| 276 |
+
return {
|
| 277 |
+
"status": "error",
|
| 278 |
+
"message": "OpenAI API key not configured. Please set OPENAI_API_KEY environment variable or use manual truncation (disable LLM search).",
|
| 279 |
+
"truncated_text": "",
|
| 280 |
+
"explanation": ""
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
# If validators provided, use them for this call
|
| 284 |
+
endpoint_list = validators if (validators and isinstance(validators, list) and len(validators) > 0) else None
|
| 285 |
+
|
| 286 |
+
# Call validator to get error token location with vote details
|
| 287 |
+
completion_before_err, explanation, vote_details = self.call_validator(msg, tokens, endpoint_list)
|
| 288 |
+
|
| 289 |
+
# Combine prompt with truncated completion
|
| 290 |
+
truncated_text = prompt + completion_before_err
|
| 291 |
+
|
| 292 |
+
# Calculate error token index
|
| 293 |
+
error_token_index = len(self.tokenizer(completion_before_err).input_ids)
|
| 294 |
+
|
| 295 |
+
return {
|
| 296 |
+
"status": "success",
|
| 297 |
+
"truncated_text": truncated_text,
|
| 298 |
+
"explanation": explanation,
|
| 299 |
+
"error_token_index": error_token_index,
|
| 300 |
+
"vote_details": vote_details
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
except Exception as e:
|
| 304 |
+
logger.error(f"Error in locate_error_token: {e}")
|
| 305 |
+
import traceback
|
| 306 |
+
traceback.print_exc()
|
| 307 |
+
return {
|
| 308 |
+
"status": "error",
|
| 309 |
+
"message": str(e),
|
| 310 |
+
"truncated_text": "",
|
| 311 |
+
"explanation": ""
|
| 312 |
+
}
|
backend/graph_metrics.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import networkx as nx
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from scipy.stats import entropy
|
| 5 |
+
|
| 6 |
+
class GraphMetrics:
|
| 7 |
+
"""
|
| 8 |
+
A unified class for calculating metrics on attribution graphs.
|
| 9 |
+
Metrics are organized into three groups:
|
| 10 |
+
1. Connectivity & Sparsity
|
| 11 |
+
2. Information Flow & Concentration
|
| 12 |
+
3. Temporal Dynamics
|
| 13 |
+
|
| 14 |
+
Each metric provides:
|
| 15 |
+
- Global Summary (single scalar)
|
| 16 |
+
- Layer-wise Profile (DataFrame/Series)
|
| 17 |
+
"""
|
| 18 |
+
def __init__(self, G, tokens=None, total_seq_len=None):
|
| 19 |
+
self.G = G
|
| 20 |
+
self.tokens = tokens
|
| 21 |
+
|
| 22 |
+
if total_seq_len is None and tokens is not None:
|
| 23 |
+
self.total_seq_len = len(tokens)
|
| 24 |
+
else:
|
| 25 |
+
self.total_seq_len = total_seq_len
|
| 26 |
+
|
| 27 |
+
self.nodes_by_layer = self._group_nodes_by_layer()
|
| 28 |
+
self.sorted_layers = sorted(self.nodes_by_layer.keys())
|
| 29 |
+
|
| 30 |
+
def _get_token_str(self, index):
|
| 31 |
+
if self.tokens and 0 <= index < len(self.tokens):
|
| 32 |
+
return self.tokens[index]
|
| 33 |
+
return str(index)
|
| 34 |
+
|
| 35 |
+
def _group_nodes_by_layer(self):
|
| 36 |
+
groups = {}
|
| 37 |
+
for n in self.G.nodes():
|
| 38 |
+
layer = n[0]
|
| 39 |
+
if layer not in groups:
|
| 40 |
+
groups[layer] = []
|
| 41 |
+
groups[layer].append(n)
|
| 42 |
+
return groups
|
| 43 |
+
|
| 44 |
+
def _calculate_gini(self, array):
|
| 45 |
+
"""Auxiliary function to calculate Gini coefficient."""
|
| 46 |
+
array = np.abs(array)
|
| 47 |
+
if np.sum(array) == 0:
|
| 48 |
+
return 0.0
|
| 49 |
+
array = np.sort(array)
|
| 50 |
+
index = np.arange(1, array.shape[0] + 1)
|
| 51 |
+
n = array.shape[0]
|
| 52 |
+
return ((np.sum((2 * index - n - 1) * array)) / (n * np.sum(array)))
|
| 53 |
+
|
| 54 |
+
def _calculate_top_mass(self, values, fraction=0.9):
|
| 55 |
+
"""Calculates count and percentage of items needed to reach mass fraction."""
|
| 56 |
+
values = np.abs(values)
|
| 57 |
+
total = values.sum()
|
| 58 |
+
if total == 0:
|
| 59 |
+
return 0, 0.0
|
| 60 |
+
|
| 61 |
+
sorted_vals = np.sort(values)[::-1]
|
| 62 |
+
cumsum = np.cumsum(sorted_vals)
|
| 63 |
+
cutoff = total * fraction
|
| 64 |
+
|
| 65 |
+
# Find index where cumsum >= cutoff
|
| 66 |
+
idx = np.searchsorted(cumsum, cutoff)
|
| 67 |
+
count = idx + 1
|
| 68 |
+
pct = (count / len(values)) * 100.0
|
| 69 |
+
return count, pct
|
| 70 |
+
|
| 71 |
+
def _get_top_nodes_by_mass(self, node_val_pairs, fraction=0.9):
|
| 72 |
+
"""
|
| 73 |
+
Calculates top nodes making up mass fraction.
|
| 74 |
+
Args:
|
| 75 |
+
node_val_pairs: list of (node, value)
|
| 76 |
+
Returns:
|
| 77 |
+
count, pct, list of (token_idx, token_str)
|
| 78 |
+
"""
|
| 79 |
+
if not node_val_pairs:
|
| 80 |
+
return 0, 0.0, []
|
| 81 |
+
|
| 82 |
+
values = np.array([abs(v) for _, v in node_val_pairs])
|
| 83 |
+
total = values.sum()
|
| 84 |
+
if total == 0:
|
| 85 |
+
return 0, 0.0, []
|
| 86 |
+
|
| 87 |
+
# Sort indices by value descending
|
| 88 |
+
sorted_indices = np.argsort(values)[::-1]
|
| 89 |
+
sorted_vals = values[sorted_indices]
|
| 90 |
+
|
| 91 |
+
cumsum = np.cumsum(sorted_vals)
|
| 92 |
+
cutoff = total * fraction
|
| 93 |
+
|
| 94 |
+
idx_cutoff = np.searchsorted(cumsum, cutoff)
|
| 95 |
+
count = idx_cutoff + 1
|
| 96 |
+
pct = (count / len(values)) * 100.0
|
| 97 |
+
|
| 98 |
+
# Extract top nodes
|
| 99 |
+
top_indices = sorted_indices[:count]
|
| 100 |
+
top_nodes = []
|
| 101 |
+
for i in top_indices:
|
| 102 |
+
node, _ = node_val_pairs[i]
|
| 103 |
+
# node is (layer, token_idx)
|
| 104 |
+
token_idx = int(node[1])
|
| 105 |
+
token_str = self._get_token_str(token_idx)
|
| 106 |
+
top_nodes.append((token_idx, token_str))
|
| 107 |
+
|
| 108 |
+
return count, pct, top_nodes
|
| 109 |
+
|
| 110 |
+
# ==========================================
|
| 111 |
+
# Group 1: Connectivity & Sparsity
|
| 112 |
+
# ==========================================
|
| 113 |
+
|
| 114 |
+
def get_connectivity_stats(self):
|
| 115 |
+
"""Calculates Scale, Complexity, and Branching metrics."""
|
| 116 |
+
if self.G.number_of_nodes() == 0:
|
| 117 |
+
return pd.DataFrame()
|
| 118 |
+
|
| 119 |
+
layer_stats = []
|
| 120 |
+
|
| 121 |
+
# --- Layer-wise ---
|
| 122 |
+
for layer in self.sorted_layers:
|
| 123 |
+
nodes = self.nodes_by_layer[layer]
|
| 124 |
+
|
| 125 |
+
# Fan-In (Inputs from prev layers)
|
| 126 |
+
in_degrees = [self.G.in_degree(n) for n in nodes]
|
| 127 |
+
avg_in = np.mean(in_degrees) if in_degrees else 0
|
| 128 |
+
# std_in = np.std(in_degrees) if in_degrees else 0
|
| 129 |
+
|
| 130 |
+
# Fan-Out (Outputs to next layers)
|
| 131 |
+
out_degrees = [self.G.out_degree(n) for n in nodes]
|
| 132 |
+
avg_out = np.mean(out_degrees) if out_degrees else 0
|
| 133 |
+
# std_out = np.std(out_degrees) if out_degrees else 0
|
| 134 |
+
|
| 135 |
+
# Derived Layer Metrics
|
| 136 |
+
node_count = len(nodes)
|
| 137 |
+
edge_count = np.sum(out_degrees)
|
| 138 |
+
|
| 139 |
+
# Edge/Node Ratio
|
| 140 |
+
edge_node_ratio = edge_count / node_count if node_count > 0 else 0
|
| 141 |
+
|
| 142 |
+
stats = {
|
| 143 |
+
"Layer": layer,
|
| 144 |
+
"Node_Count": node_count,
|
| 145 |
+
"Edge_Count": edge_count,
|
| 146 |
+
"Edge_Node_Ratio": edge_node_ratio,
|
| 147 |
+
"Avg_Fan_In": avg_in,
|
| 148 |
+
# "Std_Fan_In": std_in,
|
| 149 |
+
"Avg_Fan_Out": avg_out,
|
| 150 |
+
# "Std_Fan_Out": std_out
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
if self.total_seq_len:
|
| 154 |
+
stats["Sparsity"] = 1.0 - (node_count / self.total_seq_len)
|
| 155 |
+
|
| 156 |
+
layer_stats.append(stats)
|
| 157 |
+
|
| 158 |
+
df_layer = pd.DataFrame(layer_stats)
|
| 159 |
+
|
| 160 |
+
return df_layer
|
| 161 |
+
|
| 162 |
+
# ==========================================
|
| 163 |
+
# Group 2: Information Flow & Concentration
|
| 164 |
+
# ==========================================
|
| 165 |
+
|
| 166 |
+
def get_flow_stats(self):
|
| 167 |
+
"""Calculates Effective Branching, Gini, and Verticality."""
|
| 168 |
+
if self.G.number_of_nodes() == 0:
|
| 169 |
+
return pd.DataFrame()
|
| 170 |
+
|
| 171 |
+
layer_stats = []
|
| 172 |
+
|
| 173 |
+
for layer in self.sorted_layers:
|
| 174 |
+
nodes = self.nodes_by_layer[layer]
|
| 175 |
+
# eff_degrees_in = []
|
| 176 |
+
# eff_degrees_out = []
|
| 177 |
+
node_ginis_in = []
|
| 178 |
+
node_ginis_out = []
|
| 179 |
+
|
| 180 |
+
# New Collectors
|
| 181 |
+
layer_node_rels = []
|
| 182 |
+
layer_out_edge_rels = []
|
| 183 |
+
|
| 184 |
+
layer_vertical_mass = 0.0
|
| 185 |
+
layer_total_mass = 0.0
|
| 186 |
+
|
| 187 |
+
for n in nodes:
|
| 188 |
+
# Node Relevance
|
| 189 |
+
rel = abs(self.G.nodes[n].get('relevance', 0.0))
|
| 190 |
+
layer_node_rels.append(rel)
|
| 191 |
+
|
| 192 |
+
# Incoming Edges analysis
|
| 193 |
+
in_edges = self.G.in_edges(n, data=True)
|
| 194 |
+
weights_in = np.array([abs(d.get('weight', 0.0)) for u, v, d in in_edges])
|
| 195 |
+
|
| 196 |
+
# Flow Mass
|
| 197 |
+
if len(weights_in) > 0:
|
| 198 |
+
w_sum = weights_in.sum()
|
| 199 |
+
layer_total_mass += w_sum
|
| 200 |
+
|
| 201 |
+
# Verticality check
|
| 202 |
+
for u, v, d in in_edges:
|
| 203 |
+
if u[1] == v[1]: # Same token index
|
| 204 |
+
layer_vertical_mass += abs(d.get('weight', 0.0))
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# Outgoing Edges analysis
|
| 209 |
+
out_edges = self.G.out_edges(n, data=True)
|
| 210 |
+
weights_out = np.array([abs(d.get('weight', 0.0)) for u, v, d in out_edges])
|
| 211 |
+
|
| 212 |
+
# Collect Out Edge Relevances
|
| 213 |
+
if len(weights_out) > 0:
|
| 214 |
+
layer_out_edge_rels.extend(weights_out)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# Gini (On Inputs)
|
| 218 |
+
if len(weights_in) > 1:
|
| 219 |
+
g = self._calculate_gini(weights_in)
|
| 220 |
+
node_ginis_in.append(g)
|
| 221 |
+
elif len(weights_in) == 1:
|
| 222 |
+
node_ginis_in.append(1.0)
|
| 223 |
+
|
| 224 |
+
# Gini (On Outputs)
|
| 225 |
+
if len(weights_out) > 1:
|
| 226 |
+
g = self._calculate_gini(weights_out)
|
| 227 |
+
node_ginis_out.append(g)
|
| 228 |
+
elif len(weights_out) == 1:
|
| 229 |
+
node_ginis_out.append(1.0)
|
| 230 |
+
|
| 231 |
+
# Node Statistics
|
| 232 |
+
layer_node_rels = np.array(layer_node_rels)
|
| 233 |
+
avg_node_rel = np.mean(layer_node_rels) if len(layer_node_rels) > 0 else 0
|
| 234 |
+
node_rel_gini = self._calculate_gini(layer_node_rels) if len(layer_node_rels) > 0 else 0
|
| 235 |
+
|
| 236 |
+
# Node Mass Fractions (Moved to Group 3)
|
| 237 |
+
|
| 238 |
+
# Out Edge Statistics
|
| 239 |
+
layer_out_edge_rels = np.array(layer_out_edge_rels)
|
| 240 |
+
avg_out_edge_rel = np.mean(layer_out_edge_rels) if len(layer_out_edge_rels) > 0 else 0
|
| 241 |
+
out_edge_rel_gini = self._calculate_gini(layer_out_edge_rels) if len(layer_out_edge_rels) > 0 else 0
|
| 242 |
+
|
| 243 |
+
layer_stats.append({
|
| 244 |
+
"Layer": layer,
|
| 245 |
+
|
| 246 |
+
# Node Relevance Stats
|
| 247 |
+
"Avg_Node_Rel": avg_node_rel,
|
| 248 |
+
"Node_Rel_Gini": node_rel_gini,
|
| 249 |
+
|
| 250 |
+
# Out Edge Relevance Stats
|
| 251 |
+
"Avg_Out_Edge_Rel": avg_out_edge_rel,
|
| 252 |
+
"Gini_Out_Edge_Rel": out_edge_rel_gini,
|
| 253 |
+
|
| 254 |
+
# by node Gini Stats
|
| 255 |
+
"Avg_Edge_Gini_In_by_Node": np.mean(node_ginis_in) if node_ginis_in else 0,
|
| 256 |
+
"Avg_Edge_Gini_Out_by_Node": np.mean(node_ginis_out) if node_ginis_out else 0,
|
| 257 |
+
|
| 258 |
+
"Verticality_Ratio_by_Node": layer_vertical_mass / layer_total_mass if layer_total_mass > 0 else 0
|
| 259 |
+
})
|
| 260 |
+
|
| 261 |
+
df_layer = pd.DataFrame(layer_stats)
|
| 262 |
+
|
| 263 |
+
return df_layer
|
| 264 |
+
|
| 265 |
+
# ==========================================
|
| 266 |
+
# Group 3: Node Hubs & Significant Tokens
|
| 267 |
+
# ==========================================
|
| 268 |
+
|
| 269 |
+
def get_node_hub_stats(self):
|
| 270 |
+
"""Calculates Top Mass Nodes, Degree Hubs, and Signed Relevance Hubs."""
|
| 271 |
+
if self.G.number_of_nodes() == 0:
|
| 272 |
+
return pd.DataFrame()
|
| 273 |
+
|
| 274 |
+
layer_stats = []
|
| 275 |
+
|
| 276 |
+
for layer in self.sorted_layers:
|
| 277 |
+
nodes = self.nodes_by_layer[layer]
|
| 278 |
+
|
| 279 |
+
# 1. Gather Data
|
| 280 |
+
node_rels = [] # Pairs of (node, rel)
|
| 281 |
+
in_degrees = [] # Pairs of (node, deg)
|
| 282 |
+
out_degrees = [] # Pairs of (node, deg)
|
| 283 |
+
|
| 284 |
+
pos_node_rels = []
|
| 285 |
+
neg_node_rels = []
|
| 286 |
+
|
| 287 |
+
for n in nodes:
|
| 288 |
+
# Relevance
|
| 289 |
+
rel = self.G.nodes[n].get('relevance', 0.0)
|
| 290 |
+
node_rels.append((n, rel))
|
| 291 |
+
|
| 292 |
+
if rel >= 0:
|
| 293 |
+
pos_node_rels.append((n, rel))
|
| 294 |
+
else:
|
| 295 |
+
neg_node_rels.append((n, rel))
|
| 296 |
+
|
| 297 |
+
# Degree
|
| 298 |
+
in_deg = self.G.in_degree(n)
|
| 299 |
+
in_degrees.append((n, in_deg))
|
| 300 |
+
|
| 301 |
+
out_deg = self.G.out_degree(n)
|
| 302 |
+
out_degrees.append((n, out_deg))
|
| 303 |
+
|
| 304 |
+
# 2. General Top Mass (Abs)
|
| 305 |
+
n_90, pct_90, top_90_nodes = self._get_top_nodes_by_mass(node_rels, fraction=0.9)
|
| 306 |
+
|
| 307 |
+
# 3. Top Mass Positive & Negative
|
| 308 |
+
n_pos, pct_pos, top_pos_nodes = self._get_top_nodes_by_mass(pos_node_rels, fraction=0.9)
|
| 309 |
+
n_neg, pct_neg, top_neg_nodes = self._get_top_nodes_by_mass(neg_node_rels, fraction=0.9)
|
| 310 |
+
|
| 311 |
+
# 4. Degree Hubs (Mean + Std)
|
| 312 |
+
# In-Degree
|
| 313 |
+
in_deg_vals = [d for _, d in in_degrees]
|
| 314 |
+
if len(in_deg_vals) > 0:
|
| 315 |
+
avg_in = np.mean(in_deg_vals)
|
| 316 |
+
std_in = np.std(in_deg_vals)
|
| 317 |
+
thresh_in = avg_in + std_in
|
| 318 |
+
hub_in_nodes = []
|
| 319 |
+
for n, deg in in_degrees:
|
| 320 |
+
if deg > thresh_in:
|
| 321 |
+
token_idx = int(n[1])
|
| 322 |
+
token_str = self._get_token_str(token_idx)
|
| 323 |
+
hub_in_nodes.append((token_idx, token_str))
|
| 324 |
+
else:
|
| 325 |
+
hub_in_nodes = []
|
| 326 |
+
|
| 327 |
+
# Out-Degree
|
| 328 |
+
out_deg_vals = [d for _, d in out_degrees]
|
| 329 |
+
if len(out_deg_vals) > 0:
|
| 330 |
+
avg_out = np.mean(out_deg_vals)
|
| 331 |
+
std_out = np.std(out_deg_vals)
|
| 332 |
+
thresh_out = avg_out + std_out
|
| 333 |
+
hub_out_nodes = []
|
| 334 |
+
for n, deg in out_degrees:
|
| 335 |
+
if deg > thresh_out:
|
| 336 |
+
token_idx = int(n[1])
|
| 337 |
+
token_str = self._get_token_str(token_idx)
|
| 338 |
+
hub_out_nodes.append((token_idx, token_str))
|
| 339 |
+
else:
|
| 340 |
+
hub_out_nodes = []
|
| 341 |
+
|
| 342 |
+
layer_stats.append({
|
| 343 |
+
"Layer": layer,
|
| 344 |
+
|
| 345 |
+
# Abs Relevance Hubs
|
| 346 |
+
"Node_Rel_Top_90_Pct_Count": n_90,
|
| 347 |
+
"Node_Rel_Top_90_Pct": pct_90,
|
| 348 |
+
# "Top_Rel_Nodes": top_90_nodes,
|
| 349 |
+
|
| 350 |
+
# Signed Relevance Hubs
|
| 351 |
+
"Pos_Rel_Top_90_Pct_Count": n_pos,
|
| 352 |
+
"Pos_Rel_Top_90_Pct": pct_pos,
|
| 353 |
+
# "Top_Pos_Nodes": top_pos_nodes,
|
| 354 |
+
|
| 355 |
+
"Neg_Rel_Top_90_Pct_Count": n_neg,
|
| 356 |
+
"Neg_Rel_Top_90_Pct": pct_neg,
|
| 357 |
+
# "Top_Neg_Nodes": top_neg_nodes,
|
| 358 |
+
|
| 359 |
+
# Structural Hubs
|
| 360 |
+
"Hub_In_Count": len(hub_in_nodes),
|
| 361 |
+
# "Hub_In_Nodes": hub_in_nodes,
|
| 362 |
+
"Hub_Out_Count": len(hub_out_nodes),
|
| 363 |
+
# "Hub_Out_Nodes": hub_out_nodes
|
| 364 |
+
})
|
| 365 |
+
|
| 366 |
+
return pd.DataFrame(layer_stats)
|
| 367 |
+
|
| 368 |
+
# ==========================================
|
| 369 |
+
# Group 4: Temporal Dynamics
|
| 370 |
+
# ==========================================
|
| 371 |
+
|
| 372 |
+
def get_temporal_stats(self):
|
| 373 |
+
"""Calculates Lookback, Drift, and Locality."""
|
| 374 |
+
if self.G.number_of_nodes() == 0:
|
| 375 |
+
return pd.DataFrame()
|
| 376 |
+
|
| 377 |
+
layer_stats = []
|
| 378 |
+
|
| 379 |
+
for layer in self.sorted_layers:
|
| 380 |
+
nodes = self.nodes_by_layer[layer]
|
| 381 |
+
|
| 382 |
+
# --- Center of Mass (Drift) ---
|
| 383 |
+
positions = []
|
| 384 |
+
relevances = []
|
| 385 |
+
|
| 386 |
+
# For CoM, we look at the nodes THEMSELVES, not edges
|
| 387 |
+
for n in nodes:
|
| 388 |
+
# We need node relevance. If not stored, default to 1
|
| 389 |
+
rel = self.G.nodes[n].get('relevance', 1.0)
|
| 390 |
+
positions.append(n[1]) # Token Index
|
| 391 |
+
relevances.append(abs(rel))
|
| 392 |
+
|
| 393 |
+
positions = np.array(positions)
|
| 394 |
+
relevances = np.array(relevances)
|
| 395 |
+
|
| 396 |
+
if relevances.sum() > 0:
|
| 397 |
+
com = np.average(positions, weights=relevances)
|
| 398 |
+
else:
|
| 399 |
+
com = np.mean(positions) if len(positions) > 0 else 0
|
| 400 |
+
|
| 401 |
+
# --- Lookback & Locality (Edges entering this layer) ---
|
| 402 |
+
layer_lookbacks = []
|
| 403 |
+
|
| 404 |
+
for n in nodes:
|
| 405 |
+
in_edges = self.G.in_edges(n)
|
| 406 |
+
for u, v in in_edges:
|
| 407 |
+
# u is source, v is target (n)
|
| 408 |
+
# Lookback = v_pos - u_pos
|
| 409 |
+
dist = v[1] - u[1]
|
| 410 |
+
layer_lookbacks.append(dist)
|
| 411 |
+
|
| 412 |
+
layer_lookbacks = np.array(layer_lookbacks)
|
| 413 |
+
|
| 414 |
+
layer_stats.append({
|
| 415 |
+
"Layer": layer,
|
| 416 |
+
"Center_Of_Mass_Idx": com,
|
| 417 |
+
"Mean_Lookback": np.mean(layer_lookbacks) if len(layer_lookbacks) > 0 else 0,
|
| 418 |
+
"Local_Processing_Ratio": np.mean(layer_lookbacks == 0) if len(layer_lookbacks) > 0 else 0
|
| 419 |
+
})
|
| 420 |
+
|
| 421 |
+
df_layer = pd.DataFrame(layer_stats)
|
| 422 |
+
|
| 423 |
+
return df_layer
|
backend/metrics.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from scipy.stats import entropy
|
| 3 |
+
|
| 4 |
+
def calculate_normalized_entropy(relevance):
|
| 5 |
+
"""
|
| 6 |
+
Calculates Shannon Entropy and Normalized Entropy of the relevance distribution.
|
| 7 |
+
Use absolute values of relevance to treat as a probability distribution.
|
| 8 |
+
|
| 9 |
+
Returns:
|
| 10 |
+
tuple: (entropy, normalized_entropy)
|
| 11 |
+
"""
|
| 12 |
+
rel = np.array(relevance)
|
| 13 |
+
abs_rel = np.abs(rel)
|
| 14 |
+
n = len(abs_rel)
|
| 15 |
+
|
| 16 |
+
if n == 0:
|
| 17 |
+
return 0.0, 0.0
|
| 18 |
+
|
| 19 |
+
total_mass = abs_rel.sum()
|
| 20 |
+
if total_mass > 0:
|
| 21 |
+
probs = abs_rel / total_mass
|
| 22 |
+
else:
|
| 23 |
+
# Uniform if sum is 0
|
| 24 |
+
probs = np.ones_like(abs_rel) / n
|
| 25 |
+
|
| 26 |
+
# Shannon Entropy
|
| 27 |
+
ent = entropy(probs)
|
| 28 |
+
|
| 29 |
+
# Normalized Entropy (0 to 1)
|
| 30 |
+
# max_ent = ln(N)
|
| 31 |
+
max_ent = np.log(n)
|
| 32 |
+
norm_ent = ent / max_ent if max_ent > 0 else 0
|
| 33 |
+
|
| 34 |
+
return ent, norm_ent
|
| 35 |
+
|
| 36 |
+
def calculate_gini_coefficient(relevance):
|
| 37 |
+
"""
|
| 38 |
+
Calculates the Gini Coefficient of the relevance absolute values.
|
| 39 |
+
Values near 1 indicate very unequal distribution (high concentration/sparsity).
|
| 40 |
+
Values near 0 indicate uniform distribution.
|
| 41 |
+
"""
|
| 42 |
+
rel = np.array(relevance)
|
| 43 |
+
abs_rel = np.abs(rel)
|
| 44 |
+
n = len(abs_rel)
|
| 45 |
+
|
| 46 |
+
if n == 0 or abs_rel.mean() == 0:
|
| 47 |
+
return 0.0
|
| 48 |
+
|
| 49 |
+
# Sort ascending for Gini calculation formula
|
| 50 |
+
sorted_asc = np.sort(abs_rel)
|
| 51 |
+
index = np.arange(1, n + 1)
|
| 52 |
+
|
| 53 |
+
# Gini = (2 * sum(i * xi) - (n + 1) * sum(xi)) / (n * sum(xi))
|
| 54 |
+
gini_coeff = (2 * np.sum(index * sorted_asc) - (n + 1) * np.sum(sorted_asc)) / (n * np.sum(sorted_asc))
|
| 55 |
+
|
| 56 |
+
return gini_coeff
|
| 57 |
+
|
| 58 |
+
def calculate_top_mass_fraction(relevance, fraction=0.9):
|
| 59 |
+
"""
|
| 60 |
+
Calculates how many tokens (and percentage) account for a specific fraction of the total attribution mass.
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
tuple: (count, percentage, cdf_array, sorted_indices)
|
| 64 |
+
"""
|
| 65 |
+
rel = np.array(relevance)
|
| 66 |
+
abs_rel = np.abs(rel)
|
| 67 |
+
n = len(abs_rel)
|
| 68 |
+
|
| 69 |
+
if n == 0:
|
| 70 |
+
return 0, 0.0, np.array([]), np.array([])
|
| 71 |
+
|
| 72 |
+
total_mass = abs_rel.sum()
|
| 73 |
+
if total_mass == 0:
|
| 74 |
+
# Avoid division by zero
|
| 75 |
+
# Return as if uniform or none
|
| 76 |
+
return n, 100.0, np.linspace(0, 1, n), np.arange(n)
|
| 77 |
+
|
| 78 |
+
# Sort descending
|
| 79 |
+
sorted_indices = np.argsort(abs_rel)[::-1]
|
| 80 |
+
sorted_mass = abs_rel[sorted_indices]
|
| 81 |
+
cumulative_mass = np.cumsum(sorted_mass)
|
| 82 |
+
cdf = cumulative_mass / total_mass
|
| 83 |
+
|
| 84 |
+
# Find how many tokens account for 'fraction' mass
|
| 85 |
+
count = np.searchsorted(cdf, fraction) + 1
|
| 86 |
+
percentage = (count / n) * 100
|
| 87 |
+
|
| 88 |
+
return count, percentage, cdf, sorted_indices, sorted_mass
|
| 89 |
+
|
| 90 |
+
def calculate_center_of_mass(relevance):
|
| 91 |
+
"""
|
| 92 |
+
Calculates the center of mass (expected position) of the attribution.
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
tuple: (absolute_center_index, relative_center_0_to_1)
|
| 96 |
+
"""
|
| 97 |
+
rel = np.array(relevance)
|
| 98 |
+
abs_rel = np.abs(rel)
|
| 99 |
+
total_mass = abs_rel.sum()
|
| 100 |
+
n = len(abs_rel)
|
| 101 |
+
|
| 102 |
+
if n == 0 or total_mass == 0:
|
| 103 |
+
return 0.0, 0.0
|
| 104 |
+
|
| 105 |
+
indices = np.arange(n)
|
| 106 |
+
# E[index] = sum(p_i * i)
|
| 107 |
+
center_of_mass = np.sum(indices * abs_rel) / total_mass
|
| 108 |
+
|
| 109 |
+
# Relative center: 0 = start, 1 = end
|
| 110 |
+
# Use (n-1) as denominator because indices are 0 to n-1
|
| 111 |
+
relative_center = center_of_mass / (n - 1) if n > 1 else 0.5
|
| 112 |
+
|
| 113 |
+
return center_of_mass, relative_center
|
| 114 |
+
|
| 115 |
+
def calculate_early_late_ratio(relevance, split_ratio=0.5):
|
| 116 |
+
"""
|
| 117 |
+
Calculates the ratio of mass in the first part vs the second part of the sequence.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
relevance: The attribution array.
|
| 121 |
+
split_ratio: The point to split early vs late (0.5 = middle).
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
float: Ratio (Early Mass / Late Mass).
|
| 125 |
+
Returns infinity if Late Mass is 0.
|
| 126 |
+
Returns 0 if Early Mass is 0.
|
| 127 |
+
"""
|
| 128 |
+
rel = np.array(relevance)
|
| 129 |
+
abs_rel = np.abs(rel)
|
| 130 |
+
n = len(abs_rel)
|
| 131 |
+
|
| 132 |
+
if n == 0:
|
| 133 |
+
return 0.0
|
| 134 |
+
|
| 135 |
+
split_index = int(n * split_ratio)
|
| 136 |
+
|
| 137 |
+
early_mass = np.sum(abs_rel[:split_index])
|
| 138 |
+
late_mass = np.sum(abs_rel[split_index:])
|
| 139 |
+
|
| 140 |
+
if late_mass == 0:
|
| 141 |
+
return float('inf') if early_mass > 0 else 0.0
|
| 142 |
+
|
| 143 |
+
return early_mass / late_mass
|
backend/models/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .manager import ModelManager
|
| 2 |
+
from .base import LayerDecomposer
|
| 3 |
+
from .qwen import QwenDecomposer
|
| 4 |
+
from .factory import get_decomposer
|
backend/models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (326 Bytes). View file
|
|
|
backend/models/__pycache__/base.cpython-312.pyc
ADDED
|
Binary file (1.46 kB). View file
|
|
|
backend/models/__pycache__/factory.cpython-312.pyc
ADDED
|
Binary file (857 Bytes). View file
|
|
|
backend/models/__pycache__/manager.cpython-312.pyc
ADDED
|
Binary file (7.7 kB). View file
|
|
|
backend/models/__pycache__/qwen.cpython-312.pyc
ADDED
|
Binary file (2.27 kB). View file
|
|
|
backend/models/base.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
class LayerDecomposer(ABC):
|
| 5 |
+
"""
|
| 6 |
+
Abstract base class for decomposing Transformer layers into
|
| 7 |
+
Attention (Part 1) and MLP (Part 2) components.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
@abstractmethod
|
| 11 |
+
def get_mid_activation_module(self, layer_module):
|
| 12 |
+
"""
|
| 13 |
+
Returns the module whose input corresponds to 'resid_mid'.
|
| 14 |
+
This is typically the Post-Attention LayerNorm.
|
| 15 |
+
"""
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
@abstractmethod
|
| 19 |
+
def forward_part1(self, layer_module, hidden_states, position_embeddings=None, attention_mask=None):
|
| 20 |
+
"""
|
| 21 |
+
Executes: Norm -> Attn -> Residual Add
|
| 22 |
+
Returns: resid_mid
|
| 23 |
+
"""
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
@abstractmethod
|
| 27 |
+
def forward_part2(self, layer_module, hidden_states):
|
| 28 |
+
"""
|
| 29 |
+
Executes: Norm -> MLP -> Residual Add
|
| 30 |
+
Returns: resid_post
|
| 31 |
+
"""
|
| 32 |
+
pass
|
backend/models/factory.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base import LayerDecomposer
|
| 2 |
+
from .qwen import QwenDecomposer
|
| 3 |
+
|
| 4 |
+
# Registry mapping model type string (or class) to Decomposer
|
| 5 |
+
DECOMPOSER_REGISTRY = {
|
| 6 |
+
# Keys should match what we expect in model config or name logic
|
| 7 |
+
"qwen2": QwenDecomposer,
|
| 8 |
+
"qwen3": QwenDecomposer,
|
| 9 |
+
"qwen": QwenDecomposer, # Generic fallback
|
| 10 |
+
"llama": QwenDecomposer, # Llama usually identical structure (PreNorm, RMS, MLP)
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
def get_decomposer(model_name_or_obj) -> LayerDecomposer:
|
| 14 |
+
"""
|
| 15 |
+
Factory to return appropriate decomposer.
|
| 16 |
+
"""
|
| 17 |
+
# Simple logic based on string for now
|
| 18 |
+
name = str(model_name_or_obj).lower()
|
| 19 |
+
|
| 20 |
+
if "qwen" in name:
|
| 21 |
+
return QwenDecomposer()
|
| 22 |
+
if "llama" in name:
|
| 23 |
+
return QwenDecomposer() # Re-use for now as structure is same
|
| 24 |
+
|
| 25 |
+
# Default fallback (hope compatibility)
|
| 26 |
+
print(f"Warning: No specific decomposer for {name}. Using Qwen/Llama default.")
|
| 27 |
+
return QwenDecomposer()
|
backend/models/manager.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import importlib
|
| 3 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
|
| 4 |
+
from transformers.models.qwen3 import modeling_qwen3
|
| 5 |
+
# Import other models as needed via conditional imports or a mapping
|
| 6 |
+
try:
|
| 7 |
+
from lxt.efficient import monkey_patch
|
| 8 |
+
except ImportError:
|
| 9 |
+
monkey_patch = None
|
| 10 |
+
print("Warning: lxt package not available. LRP attribution methods will be disabled.")
|
| 11 |
+
import gc
|
| 12 |
+
from .factory import get_decomposer
|
| 13 |
+
|
| 14 |
+
class ModelManager:
|
| 15 |
+
"""
|
| 16 |
+
Manages model loading, quantization, and patching.
|
| 17 |
+
"""
|
| 18 |
+
def __init__(self):
|
| 19 |
+
self.model = None
|
| 20 |
+
self.tokenizer = None
|
| 21 |
+
self.model_name = None
|
| 22 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 23 |
+
self.decomposer = None
|
| 24 |
+
|
| 25 |
+
# Track active configuration for reloading
|
| 26 |
+
self.current_model_path = None
|
| 27 |
+
self.current_dtype = None
|
| 28 |
+
self.current_lrp_rule = None
|
| 29 |
+
self.current_quantization = False
|
| 30 |
+
self.current_revision = None
|
| 31 |
+
|
| 32 |
+
def load_model(self, model_path="Qwen/Qwen3-0.6B", quantization_4bit=False, dtype="auto", revision=None, lrp_rule=None):
|
| 33 |
+
"""
|
| 34 |
+
Loads the model and tokenizer, applies monkey patches for LRP if lrp_rule is specified.
|
| 35 |
+
lrp_rule: None (no LRP), "Attn-LRP", or "CP-LRP" (Conservative Propagation)
|
| 36 |
+
"""
|
| 37 |
+
if revision == "" or revision == "null":
|
| 38 |
+
revision = None
|
| 39 |
+
|
| 40 |
+
print(f"Loading model from {model_path} with revision={revision} and rule={lrp_rule}...")
|
| 41 |
+
|
| 42 |
+
# Store active configuration
|
| 43 |
+
self.current_model_path = model_path
|
| 44 |
+
self.current_dtype = dtype
|
| 45 |
+
self.current_lrp_rule = lrp_rule
|
| 46 |
+
self.current_quantization = quantization_4bit
|
| 47 |
+
self.current_revision = revision
|
| 48 |
+
|
| 49 |
+
# Free up memory if reloading
|
| 50 |
+
if self.model is not None:
|
| 51 |
+
del self.model
|
| 52 |
+
del self.tokenizer
|
| 53 |
+
torch.cuda.empty_cache()
|
| 54 |
+
gc.collect()
|
| 55 |
+
|
| 56 |
+
self.model_name = model_path.split('/')[-1]
|
| 57 |
+
|
| 58 |
+
# Initialize Decomposer
|
| 59 |
+
self.decomposer = get_decomposer(self.model_name)
|
| 60 |
+
|
| 61 |
+
# Apply Monkey Patch for Efficient LRP (only if lrp_rule is specified)
|
| 62 |
+
if lrp_rule is not None:
|
| 63 |
+
if monkey_patch is None:
|
| 64 |
+
print("Warning: lxt package not available. Cannot apply LRP patches. Loading model without LRP.")
|
| 65 |
+
else:
|
| 66 |
+
target_module = None
|
| 67 |
+
patch_map = None
|
| 68 |
+
|
| 69 |
+
lower_path = model_path.lower()
|
| 70 |
+
if "qwen3" in lower_path:
|
| 71 |
+
importlib.reload(modeling_qwen3) # Reset to original classes to remove previous patches
|
| 72 |
+
target_module = modeling_qwen3
|
| 73 |
+
try:
|
| 74 |
+
import lxt.efficient.models.qwen3 as lxt_qwen3
|
| 75 |
+
importlib.reload(lxt_qwen3) # Reload to update class references from new modeling_qwen3
|
| 76 |
+
patch_map = lxt_qwen3.cp_LRP if lrp_rule == "CP-LRP" else lxt_qwen3.attnLRP
|
| 77 |
+
except ImportError as e:
|
| 78 |
+
print(f"Warning: Could not import lxt.efficient.models.qwen3: {e}")
|
| 79 |
+
|
| 80 |
+
elif "olmo" in lower_path:
|
| 81 |
+
try:
|
| 82 |
+
from transformers.models.olmo3 import modeling_olmo3
|
| 83 |
+
importlib.reload(modeling_olmo3)
|
| 84 |
+
target_module = modeling_olmo3
|
| 85 |
+
import lxt.efficient.models.olmo3 as lxt_olmo3
|
| 86 |
+
importlib.reload(lxt_olmo3)
|
| 87 |
+
patch_map = lxt_olmo3.cp_LRP if lrp_rule == "CP-LRP" else lxt_olmo3.attnLRP
|
| 88 |
+
except ImportError as e:
|
| 89 |
+
print(f"Warning: Could not import modeling_olmo3 or lxt module. LRP might fail. Error: {e}")
|
| 90 |
+
|
| 91 |
+
elif "qwen2" in lower_path:
|
| 92 |
+
try:
|
| 93 |
+
from transformers.models.qwen2 import modeling_qwen2
|
| 94 |
+
importlib.reload(modeling_qwen2)
|
| 95 |
+
target_module = modeling_qwen2
|
| 96 |
+
import lxt.efficient.models.qwen2 as lxt_qwen2
|
| 97 |
+
importlib.reload(lxt_qwen2)
|
| 98 |
+
patch_map = lxt_qwen2.cp_LRP if lrp_rule == "CP-LRP" else lxt_qwen2.attnLRP
|
| 99 |
+
except ImportError as e:
|
| 100 |
+
print(f"Warning: Could not import qwen2 or lxt: {e}")
|
| 101 |
+
|
| 102 |
+
if target_module:
|
| 103 |
+
if patch_map:
|
| 104 |
+
monkey_patch(target_module, patch_map=patch_map, verbose=True)
|
| 105 |
+
print(f"Applied LRP patches with rule: {lrp_rule}")
|
| 106 |
+
else:
|
| 107 |
+
monkey_patch(target_module, verbose=True) # Fallback to default
|
| 108 |
+
print("Applied default LRP patches")
|
| 109 |
+
else:
|
| 110 |
+
# LRP not enabled - reload modules to remove any previous monkey patches
|
| 111 |
+
# so that the model is loaded with vanilla (unpatched) classes.
|
| 112 |
+
lower_path = model_path.lower()
|
| 113 |
+
if "qwen3" in lower_path:
|
| 114 |
+
importlib.reload(modeling_qwen3)
|
| 115 |
+
print("Reloaded modeling_qwen3 to remove LRP patches")
|
| 116 |
+
elif "olmo" in lower_path:
|
| 117 |
+
try:
|
| 118 |
+
from transformers.models.olmo3 import modeling_olmo3
|
| 119 |
+
importlib.reload(modeling_olmo3)
|
| 120 |
+
print("Reloaded modeling_olmo3 to remove LRP patches")
|
| 121 |
+
except ImportError:
|
| 122 |
+
pass
|
| 123 |
+
elif "qwen2" in lower_path:
|
| 124 |
+
try:
|
| 125 |
+
from transformers.models.qwen2 import modeling_qwen2
|
| 126 |
+
importlib.reload(modeling_qwen2)
|
| 127 |
+
print("Reloaded modeling_qwen2 to remove LRP patches")
|
| 128 |
+
except ImportError:
|
| 129 |
+
pass
|
| 130 |
+
print("LRP not enabled - model loaded without attribution patches")
|
| 131 |
+
|
| 132 |
+
# Add else if for other models supported by lxt
|
| 133 |
+
|
| 134 |
+
# Map string dtype to torch dtype
|
| 135 |
+
torch_dtype = "auto"
|
| 136 |
+
bnb_dtype = torch.bfloat16 # Default for 4bit compute
|
| 137 |
+
|
| 138 |
+
if dtype == "float16":
|
| 139 |
+
torch_dtype = torch.float16
|
| 140 |
+
bnb_dtype = torch.float16
|
| 141 |
+
elif dtype == "bfloat16":
|
| 142 |
+
torch_dtype = torch.bfloat16
|
| 143 |
+
bnb_dtype = torch.bfloat16
|
| 144 |
+
elif dtype == "float32":
|
| 145 |
+
torch_dtype = torch.float32
|
| 146 |
+
bnb_dtype = torch.float32
|
| 147 |
+
|
| 148 |
+
# Quantization Config
|
| 149 |
+
quantization_config = None
|
| 150 |
+
if quantization_4bit:
|
| 151 |
+
quantization_config = BitsAndBytesConfig(
|
| 152 |
+
load_in_4bit=True,
|
| 153 |
+
bnb_4bit_compute_dtype=bnb_dtype,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# Load Model
|
| 157 |
+
if "qwen3" in model_path.lower():
|
| 158 |
+
self.model = modeling_qwen3.Qwen3ForCausalLM.from_pretrained(
|
| 159 |
+
model_path,
|
| 160 |
+
device_map=self.device,
|
| 161 |
+
torch_dtype=torch_dtype,
|
| 162 |
+
quantization_config=quantization_config,
|
| 163 |
+
revision=revision
|
| 164 |
+
)
|
| 165 |
+
else:
|
| 166 |
+
# Fallback for generic loading if specific class fails
|
| 167 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
| 168 |
+
model_path,
|
| 169 |
+
device_map=self.device,
|
| 170 |
+
torch_dtype=torch_dtype,
|
| 171 |
+
quantization_config=quantization_config,
|
| 172 |
+
revision=revision
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_path, revision=revision)
|
| 176 |
+
|
| 177 |
+
# Prepare model for LRP
|
| 178 |
+
self.model.eval() # Use eval usually, but test.ipynb uses train() + gradients
|
| 179 |
+
# test.ipynb: model.train(), gradient_checkpointing_enable(), requires_grad=False
|
| 180 |
+
|
| 181 |
+
# "model.train()" is often needed for Gradient Checkpointing to work in HF
|
| 182 |
+
self.model.train()
|
| 183 |
+
self.model.gradient_checkpointing_enable()
|
| 184 |
+
|
| 185 |
+
# Deactivate gradients on parameters
|
| 186 |
+
for param in self.model.parameters():
|
| 187 |
+
param.requires_grad = False
|
| 188 |
+
|
| 189 |
+
print(f"Model {self.model_name} loaded successfully on {self.device}")
|
| 190 |
+
return self.model_name
|
| 191 |
+
|
| 192 |
+
def get_model(self):
|
| 193 |
+
return self.model
|
| 194 |
+
|
| 195 |
+
def get_tokenizer(self):
|
| 196 |
+
return self.tokenizer
|
backend/models/qwen.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
from .base import LayerDecomposer
|
| 3 |
+
|
| 4 |
+
class QwenDecomposer(LayerDecomposer):
|
| 5 |
+
"""
|
| 6 |
+
Decomposer for Qwen2/Qwen3 models.
|
| 7 |
+
Assumes Pre-Norm architecture:
|
| 8 |
+
Part 1: x -> InputNorm -> Attn -> Add (resid_mid)
|
| 9 |
+
Part 2: resid_mid -> PostAttnNorm -> MLP -> Add (resid_post)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
def get_mid_activation_module(self, layer_module):
|
| 13 |
+
# Qwen models usually have 'post_attention_layernorm'
|
| 14 |
+
return getattr(layer_module, "post_attention_layernorm", None)
|
| 15 |
+
|
| 16 |
+
def forward_part1(self, layer_module, hidden_states, position_embeddings=None, attention_mask=None):
|
| 17 |
+
norm = getattr(layer_module, "input_layernorm", None)
|
| 18 |
+
# Try both generic 'self_attn' and Qwen specific naming if needed,
|
| 19 |
+
# though HF implementation usually maps to 'self_attn'
|
| 20 |
+
attn = getattr(layer_module, "self_attn", None)
|
| 21 |
+
|
| 22 |
+
if not norm or not attn:
|
| 23 |
+
# Fallback for some versions or wrapped modules
|
| 24 |
+
# Check named children
|
| 25 |
+
children = dict(layer_module.named_modules())
|
| 26 |
+
attn = attn or children.get("self_attn")
|
| 27 |
+
|
| 28 |
+
if not norm or not attn:
|
| 29 |
+
raise AttributeError(f"QwenDecomposer: Layer module {type(layer_module)} missing input_layernorm or self_attn")
|
| 30 |
+
|
| 31 |
+
# Norm
|
| 32 |
+
norm_out = norm(hidden_states)
|
| 33 |
+
|
| 34 |
+
# Attn
|
| 35 |
+
if position_embeddings is not None:
|
| 36 |
+
# Qwen/Llama usually accept position_embeddings
|
| 37 |
+
attn_out = attn(norm_out, position_embeddings=position_embeddings, attention_mask=attention_mask)
|
| 38 |
+
else:
|
| 39 |
+
attn_out = attn(norm_out, attention_mask=attention_mask)
|
| 40 |
+
|
| 41 |
+
if isinstance(attn_out, tuple): attn_out = attn_out[0]
|
| 42 |
+
|
| 43 |
+
return hidden_states + attn_out
|
| 44 |
+
|
| 45 |
+
def forward_part2(self, layer_module, hidden_states):
|
| 46 |
+
norm = getattr(layer_module, "post_attention_layernorm", None)
|
| 47 |
+
mlp = getattr(layer_module, "mlp", None)
|
| 48 |
+
|
| 49 |
+
if not norm or not mlp:
|
| 50 |
+
raise AttributeError("QwenDecomposer: Layer module missing post_attention_layernorm or mlp")
|
| 51 |
+
|
| 52 |
+
norm_out = norm(hidden_states)
|
| 53 |
+
mlp_out = mlp(norm_out)
|
| 54 |
+
|
| 55 |
+
return hidden_states + mlp_out
|
backend/token_locator_prompts/err_token_localization.yaml
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: v1
|
| 2 |
+
system: |-
|
| 3 |
+
- You are an expert in localizing error tokens in model-generated text completions.
|
| 4 |
+
- You will be provided with a prompt and a model-generated completion, and your task is to localize and output the FIRST token in the completion that is erroneous or causes the error.
|
| 5 |
+
- As the prompt and completion are taken from a particular dataset, refer to Section "Dataset Description" for a brief overview of the dataset.
|
| 6 |
+
- Refer to Section "On the Input Format" for details on how the prompt and completion will be presented to you.
|
| 7 |
+
- Follow the "General Guidance (Dataset Agnostic)" section for general instructions on how to approach the task of error token localization.
|
| 8 |
+
- Refer to the "Dataset Specific Instructions" section for any additional instructions that are specific to this dataset.
|
| 9 |
+
- Finally, adhere to the "On the Output Format" section to ensure your response is structured correctly.
|
| 10 |
+
- Note that the dataset specific sections (i.e., "Dataset Description" and "Dataset Specific Instructions") can be missing for some datasets. In such cases, simply skip those sections and proceed with the rest of the instructions.
|
| 11 |
+
|
| 12 |
+
# Dataset Description
|
| 13 |
+
{{dataset_description}}
|
| 14 |
+
|
| 15 |
+
# On the Input Format
|
| 16 |
+
- The input will be provided in the following JSON format:
|
| 17 |
+
{
|
| 18 |
+
"prompt": "<The prompt text here>",
|
| 19 |
+
"completion": "<The model-generated completion text here>",
|
| 20 |
+
"indexed_completion": "<The model-generated completion text with each token indexed, i.e., `token_1[index_1]token_2[index_2]`>"
|
| 21 |
+
"ground_truth": "<The ground truth answer text here>" (This field may be absent in some cases)
|
| 22 |
+
}
|
| 23 |
+
- An example of the input format is as follows:
|
| 24 |
+
{
|
| 25 |
+
"prompt": "<|im_start|>user\nWhat is the capital of France?<|im_end|>\n<|im_start|>assistant\n",
|
| 26 |
+
"completion": "<think> okay, let me see. The capital of France is Lyon. </think>\n\Lyon.",
|
| 27 |
+
"indexed_completion": "<think>[0] okay,[1] let[2] me[3] see.[4] The[5] capital[6] of[7] France[8] is[9] Lyon.[10] </think>[11]\n\n[12] Lyon[13] .[14]",
|
| 28 |
+
"ground_truth": "Paris, the capital of France."
|
| 29 |
+
}
|
| 30 |
+
- The "indexed_completion" field provides a tokenized version of the completion, where each token is followed by its index in square brackets. This will help you localize the position of tokens in the completion.
|
| 31 |
+
- Both the "prompt" and "completion" fields may contain special tokens, e.g., "<|im_start|>", "<|im_end|>", "<think>", "</think>", etc. These tokens are part of the model's output format, specifically,
|
| 32 |
+
- "<|im_start|>" and "<|im_end|>" denote the start and end of a message in a multi-turn conversation, e.g., "<|im_start|>user\n xxx <|im_end|>" indicates the start of a user message, while "<|im_start|>assistant\n xxx <|im_end|>" indicates the start of an assistant message.
|
| 33 |
+
- "<think>" and "</think>" denote the start and end of the model's internal reasoning process.
|
| 34 |
+
- More on the internal thought process of the model:
|
| 35 |
+
- The internal thought process of the model is represented by the reasoning tokens between the "<think>" and "</think>" tokens.
|
| 36 |
+
- This part reflects the model's reasoning steps before arriving at the final answer.
|
| 37 |
+
- The final answer to the prompt is the text that comes AFTER the "</think>" token.
|
| 38 |
+
|
| 39 |
+
# General Guidance (Dataset Agnostic)
|
| 40 |
+
- Refer to the following steps for localizing an erroneous token in the model-generated completion:
|
| 41 |
+
- Step 1: Carefully read dataset description and the provided prompt to understand the context and requirements.
|
| 42 |
+
- Step 2: If "completion" and "indexed_completion" fields contains reasoning tokens (i.e., tokens between "<think>" and "</think>"), seperate the reasoning part from the final answer part.
|
| 43 |
+
- Step 3: Examine the final answer part of the completion first to identify any errors. If no errors are found in this part, skip the rest of the steps and output "<CORRECT>" as the final answer.
|
| 44 |
+
- Step 4: No matther whether the final answer part contains errors, proceed to examine the reasoning part for potential errors.
|
| 45 |
+
- Step 5: Once an erroneous token is identified, use the "indexed_completion" field to find its index.
|
| 46 |
+
- Addiational strict requirements for localizing the erroneous token:
|
| 47 |
+
- Always report the FIRST erroneous token in the "completion" or "indexed_completion" field.
|
| 48 |
+
- The localized erroneous token MUST NOT be the special tokens used for formatting (e.g., "<|im_start|>", "<|im_end|>", "<think>", "</think>").
|
| 49 |
+
- NEVER treat a token is erroneous just because it is a subword or punctuation mark, as the indexed token str does NOT have to be a single word; it can be a subword or punctuation mark as per the tokenization used by the model.
|
| 50 |
+
- On the difference between examining the final answer part and the reasoning part:
|
| 51 |
+
- For the final answer part (i.e., the part AFTER "</think>"):
|
| 52 |
+
- ALWAYS examine whether the tokens in this part fulfill the format/style requirements specified in the prompt. If you identify a token in this part that violates the format/style requirements, you SHOULD consider it as a potential erroneous token.
|
| 53 |
+
- ALSO examine the factual correctness and logical consistency of the tokens in this part. If you identify a token in this part that is factually incorrect or logically inconsistent with the prompt, you SHOULD consider it as a potential erroneous token.
|
| 54 |
+
- For the reasoning part (i.e., the part BETWEEN "<think>" and "</think>"):
|
| 55 |
+
- NEVER examine whether the reasoning tokens have fulfilled the format/style requirements specified in the prompt, as the reasoning tokens between "<think>" and "</think>" are often in a free-form text format. For example, if the prompt requires the model to "respond in a poem format", you MUST NOT consider a token in the reasoning part as erroneous just because the reasoning tokens are not in a poem format.
|
| 56 |
+
- Focus on the factual correctness and logical consistency of the reasoning tokens. If you identify a token in this part that is factually incorrect or logically inconsistent with the prompt, you SHOULD consider it as a potential erroneous token.
|
| 57 |
+
|
| 58 |
+
# Dataset Specific Instructions
|
| 59 |
+
{{dataset_specific_instructions}}
|
| 60 |
+
|
| 61 |
+
# On the Output Format
|
| 62 |
+
- The output MUST be in the following JSON format:
|
| 63 |
+
{
|
| 64 |
+
"error_token": "<The localized erroneous token>",
|
| 65 |
+
"token_index": <The index of the erroneous token in the completion>,
|
| 66 |
+
"explanation": "<A brief explanation of why this token is considered erroneous. Less than 50 words.>"
|
| 67 |
+
}
|
| 68 |
+
- An example of the output format is as follows:
|
| 69 |
+
{
|
| 70 |
+
"error_token": "Lyon",
|
| 71 |
+
"token_index": 10,
|
| 72 |
+
"explanation": "The token 'Lyon' is erroneous because the correct capital of France is Paris, not Lyon. This indicates a factual error in the model's completion."
|
| 73 |
+
}
|
| 74 |
+
- Ensure that the "token_index" corresponds to the index provided in the "indexed_completion" field.
|
| 75 |
+
- The "explanation" should be concise yet informative, providing enough context to justify the localization of the error token.
|
frontend/css/style.css
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
background-color: #f5f7fa;
|
| 6 |
+
color: #333;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
header {
|
| 10 |
+
background-color: #2c3e50;
|
| 11 |
+
color: white;
|
| 12 |
+
padding: 1rem 2rem;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
header h1 {
|
| 16 |
+
margin: 0;
|
| 17 |
+
font-size: 1.5rem;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
main {
|
| 21 |
+
padding: 2rem;
|
| 22 |
+
max-width: 1200px;
|
| 23 |
+
margin: 0 auto;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.panel {
|
| 27 |
+
background: white;
|
| 28 |
+
border-radius: 8px;
|
| 29 |
+
padding: 1.5rem;
|
| 30 |
+
margin-bottom: 2rem;
|
| 31 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.hidden {
|
| 35 |
+
display: none;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.control-group {
|
| 39 |
+
margin-bottom: 1rem;
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
gap: 1rem;
|
| 43 |
+
flex-wrap: wrap;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
label {
|
| 47 |
+
font-weight: 500;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
input[type="text"], input[type="number"], textarea, select {
|
| 51 |
+
padding: 0.5rem;
|
| 52 |
+
border: 1px solid #ddd;
|
| 53 |
+
border-radius: 4px;
|
| 54 |
+
font-size: 1rem;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
textarea {
|
| 58 |
+
width: 100%;
|
| 59 |
+
max-width: 600px;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
button {
|
| 63 |
+
background-color: #3498db;
|
| 64 |
+
color: white;
|
| 65 |
+
border: none;
|
| 66 |
+
padding: 0.5rem 1rem;
|
| 67 |
+
border-radius: 4px;
|
| 68 |
+
cursor: pointer;
|
| 69 |
+
font-size: 1rem;
|
| 70 |
+
transition: background 0.2s;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
button:hover {
|
| 74 |
+
background-color: #2980b9;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
button:disabled {
|
| 78 |
+
background-color: #bdc3c7;
|
| 79 |
+
cursor: not-allowed;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.status {
|
| 83 |
+
font-style: italic;
|
| 84 |
+
color: #7f8c8d;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.status.success {
|
| 88 |
+
color: #27ae60;
|
| 89 |
+
}
|
| 90 |
+
.status.error {
|
| 91 |
+
color: #c0392b;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Logits Table */
|
| 95 |
+
.scrollable-table {
|
| 96 |
+
max-height: 300px;
|
| 97 |
+
overflow-y: auto;
|
| 98 |
+
border: 1px solid #eee;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
table {
|
| 102 |
+
width: 100%;
|
| 103 |
+
border-collapse: collapse;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
th, td {
|
| 107 |
+
padding: 0.35rem 0.75rem; /* Reduced vertical padding */
|
| 108 |
+
text-align: left;
|
| 109 |
+
border-bottom: 1px solid #eee;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
th {
|
| 113 |
+
background-color: #f8f9fa;
|
| 114 |
+
position: sticky;
|
| 115 |
+
top: 0;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
tr:hover {
|
| 119 |
+
background-color: #f1f1f1;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.btn-select-ref {
|
| 123 |
+
padding: 0.25rem 0.5rem;
|
| 124 |
+
font-size: 0.8rem;
|
| 125 |
+
background-color: #9b59b6;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.btn-select-ref.selected {
|
| 129 |
+
background-color: #27ae60;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* Circuit Controls */
|
| 133 |
+
.controls-row {
|
| 134 |
+
display: flex;
|
| 135 |
+
gap: 2rem;
|
| 136 |
+
margin-bottom: 1.5rem;
|
| 137 |
+
border-bottom: 1px solid #eee;
|
| 138 |
+
padding-bottom: 1rem;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.control-col {
|
| 142 |
+
flex: 1;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.sub-opt {
|
| 146 |
+
margin-left: 1rem;
|
| 147 |
+
margin-top: 0.5rem;
|
| 148 |
+
font-size: 0.9rem;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.setting-item {
|
| 152 |
+
margin-bottom: 0.8rem;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.input-row {
|
| 156 |
+
display: flex;
|
| 157 |
+
align-items: center;
|
| 158 |
+
gap: 10px;
|
| 159 |
+
margin-top: 4px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.input-row input[type="range"] {
|
| 163 |
+
flex: 1;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* Canvas */
|
| 167 |
+
#canvas-container {
|
| 168 |
+
position: relative;
|
| 169 |
+
border: 1px solid #ddd;
|
| 170 |
+
background: white;
|
| 171 |
+
border-radius: 4px;
|
| 172 |
+
overflow-x: auto;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
canvas {
|
| 176 |
+
display: block;
|
| 177 |
+
cursor: crosshair;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
#tooltip {
|
| 181 |
+
position: fixed;
|
| 182 |
+
background: rgba(0, 0, 0, 0.85);
|
| 183 |
+
color: white;
|
| 184 |
+
padding: 8px;
|
| 185 |
+
border-radius: 4px;
|
| 186 |
+
font-size: 12px;
|
| 187 |
+
pointer-events: none;
|
| 188 |
+
opacity: 0;
|
| 189 |
+
transition: opacity 0.2s;
|
| 190 |
+
z-index: 1000;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/* Legend */
|
| 194 |
+
#vis-legend {
|
| 195 |
+
margin-top: 1rem;
|
| 196 |
+
font-size: 0.9rem;
|
| 197 |
+
}
|
| 198 |
+
.legend-box {
|
| 199 |
+
display: inline-block;
|
| 200 |
+
width: 15px;
|
| 201 |
+
height: 15px;
|
| 202 |
+
margin-right: 5px;
|
| 203 |
+
margin-left: 10px;
|
| 204 |
+
vertical-align: middle;
|
| 205 |
+
}
|
| 206 |
+
.legend-box.pos { background: rgba(255, 0, 0, 0.6); }
|
| 207 |
+
.legend-box.neg { background: rgba(0, 0, 255, 0.6); }
|
| 208 |
+
|
| 209 |
+
/* Input Attribution Styles */
|
| 210 |
+
.attribution-text-box {
|
| 211 |
+
line-height: 1.6;
|
| 212 |
+
font-size: 1.1em;
|
| 213 |
+
padding: 15px;
|
| 214 |
+
border: 1px solid #ddd;
|
| 215 |
+
border-radius: 4px;
|
| 216 |
+
background-color: #fff;
|
| 217 |
+
white-space: pre-wrap; /* Preserve spacing */
|
| 218 |
+
font-family: monospace;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.token-span {
|
| 222 |
+
padding: 2px 0;
|
| 223 |
+
margin: 0;
|
| 224 |
+
border-radius: 2px;
|
| 225 |
+
transition: background-color 0.2s;
|
| 226 |
+
cursor: default;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.token-span:hover {
|
| 230 |
+
outline: 1px solid #333;
|
| 231 |
+
position: relative;
|
| 232 |
+
z-index: 10;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* Loading Overlay */
|
| 236 |
+
#loading-overlay {
|
| 237 |
+
position: fixed;
|
| 238 |
+
top: 0;
|
| 239 |
+
left: 0;
|
| 240 |
+
width: 100%;
|
| 241 |
+
height: 100%;
|
| 242 |
+
background: rgba(255, 255, 255, 0.8);
|
| 243 |
+
display: flex;
|
| 244 |
+
flex-direction: column;
|
| 245 |
+
justify-content: center;
|
| 246 |
+
align-items: center;
|
| 247 |
+
z-index: 9999;
|
| 248 |
+
backdrop-filter: blur(2px);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
#loading-overlay.hidden {
|
| 252 |
+
display: none !important;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.spinner {
|
| 256 |
+
border: 6px solid #f3f3f3;
|
| 257 |
+
border-top: 6px solid #3498db;
|
| 258 |
+
border-radius: 50%;
|
| 259 |
+
width: 50px;
|
| 260 |
+
height: 50px;
|
| 261 |
+
animation: spin 1s linear infinite;
|
| 262 |
+
margin-bottom: 15px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
@keyframes spin {
|
| 266 |
+
0% { transform: rotate(0deg); }
|
| 267 |
+
100% { transform: rotate(360deg); }
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
#loading-message {
|
| 271 |
+
font-size: 1.2rem;
|
| 272 |
+
font-weight: 500;
|
| 273 |
+
color: #2c3e50;
|
| 274 |
+
}
|
frontend/favicon.ico
ADDED
|
|
frontend/index.html
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>NeuralPostmortem: An Interactive Framework for LLM Failures Analysis</title>
|
| 7 |
+
<link rel="stylesheet" href="css/style.css">
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
| 9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
| 10 |
+
<style>
|
| 11 |
+
/* Additional styles for step-by-step UI */
|
| 12 |
+
.step-container {
|
| 13 |
+
margin-bottom: 30px;
|
| 14 |
+
border: 2px solid #e0e0e0;
|
| 15 |
+
border-radius: 8px;
|
| 16 |
+
overflow: hidden;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.step-header {
|
| 20 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 21 |
+
color: white;
|
| 22 |
+
padding: 15px 20px;
|
| 23 |
+
cursor: pointer;
|
| 24 |
+
display: flex;
|
| 25 |
+
justify-content: space-between;
|
| 26 |
+
align-items: center;
|
| 27 |
+
font-weight: bold;
|
| 28 |
+
font-size: 1.1em;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.step-header.completed {
|
| 32 |
+
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.step-header.active {
|
| 36 |
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.step-header.disabled {
|
| 40 |
+
background: #ccc;
|
| 41 |
+
cursor: not-allowed;
|
| 42 |
+
opacity: 0.6;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.step-number {
|
| 46 |
+
background: rgba(255,255,255,0.3);
|
| 47 |
+
width: 35px;
|
| 48 |
+
height: 35px;
|
| 49 |
+
border-radius: 50%;
|
| 50 |
+
display: flex;
|
| 51 |
+
align-items: center;
|
| 52 |
+
justify-content: center;
|
| 53 |
+
font-size: 1.2em;
|
| 54 |
+
margin-right: 15px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.step-title {
|
| 58 |
+
flex: 1;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.step-status {
|
| 62 |
+
font-size: 0.9em;
|
| 63 |
+
padding: 5px 15px;
|
| 64 |
+
background: rgba(255,255,255,0.2);
|
| 65 |
+
border-radius: 15px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.step-body {
|
| 69 |
+
padding: 20px;
|
| 70 |
+
background: #f9f9f9;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.step-body.collapsed {
|
| 74 |
+
display: none;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.subsection {
|
| 78 |
+
margin: 15px 0;
|
| 79 |
+
padding: 15px;
|
| 80 |
+
background: white;
|
| 81 |
+
border-radius: 5px;
|
| 82 |
+
border-left: 4px solid #667eea;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.subsection h4 {
|
| 86 |
+
margin-top: 0;
|
| 87 |
+
color: #667eea;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.action-buttons {
|
| 91 |
+
margin-top: 20px;
|
| 92 |
+
display: flex;
|
| 93 |
+
gap: 10px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.btn-primary {
|
| 97 |
+
background: #667eea;
|
| 98 |
+
color: white;
|
| 99 |
+
border: none;
|
| 100 |
+
padding: 10px 20px;
|
| 101 |
+
border-radius: 5px;
|
| 102 |
+
cursor: pointer;
|
| 103 |
+
font-size: 1em;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.btn-primary:hover {
|
| 107 |
+
background: #5568d3;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.btn-primary:disabled {
|
| 111 |
+
background: #ccc;
|
| 112 |
+
cursor: not-allowed;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.btn-secondary {
|
| 116 |
+
background: #6c757d;
|
| 117 |
+
color: white;
|
| 118 |
+
border: none;
|
| 119 |
+
padding: 10px 20px;
|
| 120 |
+
border-radius: 5px;
|
| 121 |
+
cursor: pointer;
|
| 122 |
+
font-size: 1em;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.vote-details {
|
| 126 |
+
margin-top: 10px;
|
| 127 |
+
padding: 10px;
|
| 128 |
+
background: #f0f0f0;
|
| 129 |
+
border-radius: 5px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.vote-item {
|
| 133 |
+
padding: 5px;
|
| 134 |
+
margin: 5px 0;
|
| 135 |
+
background: white;
|
| 136 |
+
border-radius: 3px;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* Method Selector Styles */
|
| 140 |
+
.method-btn {
|
| 141 |
+
background: rgba(255,255,255,0.15);
|
| 142 |
+
color: #ccc;
|
| 143 |
+
border: 1px solid rgba(255,255,255,0.25);
|
| 144 |
+
padding: 6px 16px;
|
| 145 |
+
border-radius: 20px;
|
| 146 |
+
cursor: pointer;
|
| 147 |
+
font-size: 0.9em;
|
| 148 |
+
font-weight: 500;
|
| 149 |
+
transition: all 0.2s ease;
|
| 150 |
+
}
|
| 151 |
+
.method-btn:hover {
|
| 152 |
+
background: rgba(255,255,255,0.25);
|
| 153 |
+
color: white;
|
| 154 |
+
}
|
| 155 |
+
.method-btn.active {
|
| 156 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 157 |
+
color: white;
|
| 158 |
+
border-color: #764ba2;
|
| 159 |
+
box-shadow: 0 2px 8px rgba(102,126,234,0.4);
|
| 160 |
+
}
|
| 161 |
+
.method-btn:disabled {
|
| 162 |
+
opacity: 0.4;
|
| 163 |
+
cursor: not-allowed;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* Method-specific visibility */
|
| 167 |
+
.attnlrp-only { display: block; }
|
| 168 |
+
.nnsight-only { display: none; }
|
| 169 |
+
.pyvene-only { display: none; }
|
| 170 |
+
|
| 171 |
+
body[data-method="nnsight"] .attnlrp-only { display: none; }
|
| 172 |
+
body[data-method="nnsight"] .nnsight-only { display: block; }
|
| 173 |
+
|
| 174 |
+
body[data-method="pyvene"] .attnlrp-only { display: none; }
|
| 175 |
+
body[data-method="pyvene"] .pyvene-only { display: block; }
|
| 176 |
+
</style>
|
| 177 |
+
</head>
|
| 178 |
+
<body>
|
| 179 |
+
<div id="app">
|
| 180 |
+
<header>
|
| 181 |
+
<h1>🔍 NeuralPostmortem</h1>
|
| 182 |
+
<p style="margin: 10px 0; color: #666;">An Interactive Framework for LLM Failure Analysis</p>
|
| 183 |
+
<!-- Method Selector Bar -->
|
| 184 |
+
<div id="method-bar" style="margin-top: 12px; display: flex; align-items: center; gap: 15px;">
|
| 185 |
+
<span style="font-weight: 600; color: #ccc; font-size: 0.95em;">Attribution Method:</span>
|
| 186 |
+
<div id="method-selector" style="display: flex; gap: 6px;">
|
| 187 |
+
<button class="method-btn active" data-method="attnlrp" data-port="8000">Attn-LRP</button>
|
| 188 |
+
<button class="method-btn" data-method="nnsight" data-port="8001">NNsight</button>
|
| 189 |
+
<button class="method-btn" data-method="pyvene" data-port="8002">Pyvene</button>
|
| 190 |
+
</div>
|
| 191 |
+
<span id="method-status" style="font-size: 0.85em; color: #8e8e8e; margin-left: 10px;">
|
| 192 |
+
Backend: <code id="current-api-base" style="color: #38ef7d;">localhost:8000</code>
|
| 193 |
+
</span>
|
| 194 |
+
</div>
|
| 195 |
+
</header>
|
| 196 |
+
|
| 197 |
+
<main style="max-width: 1400px; margin: 0 auto; padding: 20px;">
|
| 198 |
+
|
| 199 |
+
<!-- Step 1: Case Setup & Model Configuration -->
|
| 200 |
+
<div class="step-container" id="step-1">
|
| 201 |
+
<div class="step-header active" onclick="toggleStep(1)">
|
| 202 |
+
<div style="display: flex; align-items: center;">
|
| 203 |
+
<span class="step-number">1</span>
|
| 204 |
+
<span class="step-title">Case Setup & Model Configuration</span>
|
| 205 |
+
</div>
|
| 206 |
+
<span class="step-status" id="status-1">Pending</span>
|
| 207 |
+
</div>
|
| 208 |
+
<div class="step-body" id="body-1">
|
| 209 |
+
<div class="subsection">
|
| 210 |
+
<h4>📁 Load from Trace (Auto-fills all fields)</h4>
|
| 211 |
+
<div class="control-group">
|
| 212 |
+
<label>Dataset:
|
| 213 |
+
<select id="trace-dataset">
|
| 214 |
+
<option value="">-- Select --</option>
|
| 215 |
+
<!-- Populated by JS -->
|
| 216 |
+
</select>
|
| 217 |
+
</label>
|
| 218 |
+
<label>Trace File:
|
| 219 |
+
<select id="trace-file" disabled>
|
| 220 |
+
<option value="">-- Select Dataset First --</option>
|
| 221 |
+
</select>
|
| 222 |
+
</label>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<div class="subsection">
|
| 227 |
+
<h4>✍️ Manual Input</h4>
|
| 228 |
+
<div class="control-group">
|
| 229 |
+
<label for="input-prompt"><strong>Prompt:</strong></label>
|
| 230 |
+
<textarea id="input-prompt" rows="4" placeholder="Enter your prompt here..."></textarea>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="control-group">
|
| 233 |
+
<label for="input-completion"><strong>Completion:</strong></label>
|
| 234 |
+
<textarea id="input-completion" rows="4" placeholder="Enter the completion to analyze..."></textarea>
|
| 235 |
+
</div>
|
| 236 |
+
<div class="control-group">
|
| 237 |
+
<label for="input-ground-truth"><strong>Ground Truth (Optional):</strong></label>
|
| 238 |
+
<textarea id="input-ground-truth" rows="2" placeholder="Expected correct answer..."></textarea>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<div class="subsection">
|
| 243 |
+
<h4>🤖 Model Configuration</h4>
|
| 244 |
+
<div class="control-group">
|
| 245 |
+
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
| 246 |
+
<label>Series:
|
| 247 |
+
<select id="model-series">
|
| 248 |
+
<option value="Qwen3">Qwen3</option>
|
| 249 |
+
<option value="Qwen2">Qwen2</option>
|
| 250 |
+
<option value="OLMo3">OLMo3</option>
|
| 251 |
+
</select>
|
| 252 |
+
</label>
|
| 253 |
+
<label>Model:
|
| 254 |
+
<select id="model-select">
|
| 255 |
+
<!-- Populated dynamically -->
|
| 256 |
+
</select>
|
| 257 |
+
</label>
|
| 258 |
+
<label>DType:
|
| 259 |
+
<select id="model-dtype">
|
| 260 |
+
<option value="float16">float16</option>
|
| 261 |
+
<option value="bfloat16" selected>bfloat16</option>
|
| 262 |
+
<option value="float32">float32</option>
|
| 263 |
+
</select>
|
| 264 |
+
</label>
|
| 265 |
+
<label>
|
| 266 |
+
<input type="checkbox" id="quant-4bit"> 4-bit Quantization
|
| 267 |
+
</label>
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
<div class="control-group">
|
| 271 |
+
<label>Model Path:
|
| 272 |
+
<input type="text" id="model-path" value="Qwen/Qwen3-0.6B" style="width: 400px;">
|
| 273 |
+
</label>
|
| 274 |
+
</div>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
<div class="action-buttons">
|
| 278 |
+
<button class="btn-primary" id="btn-load-model">Load Model & Proceed</button>
|
| 279 |
+
<span id="model-load-status" style="margin-left: 15px; line-height: 40px; color: #666;"></span>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
|
| 284 |
+
<!-- Step 2: Contrast Token Pair Identification -->
|
| 285 |
+
<div class="step-container" id="step-2">
|
| 286 |
+
<div class="step-header disabled" onclick="toggleStep(2)">
|
| 287 |
+
<div style="display: flex; align-items: center;">
|
| 288 |
+
<span class="step-number">2</span>
|
| 289 |
+
<span class="step-title">Contrast Token Pair Identification</span>
|
| 290 |
+
</div>
|
| 291 |
+
<span class="step-status" id="status-2">Locked</span>
|
| 292 |
+
</div>
|
| 293 |
+
<div class="step-body collapsed" id="body-2">
|
| 294 |
+
|
| 295 |
+
<!-- Subsection 3.1: Error Token Localization -->
|
| 296 |
+
<div class="subsection">
|
| 297 |
+
<h4>🎯 Target Token Localization & Text Truncation</h4>
|
| 298 |
+
<p style="color: #666; margin-bottom: 15px;">
|
| 299 |
+
Use multiple LLM validators to identify the first erroneous token in the completion.
|
| 300 |
+
</p>
|
| 301 |
+
|
| 302 |
+
<div class="control-group">
|
| 303 |
+
<label style="display: flex; align-items: center; gap: 10px;">
|
| 304 |
+
<input type="checkbox" id="use-llm-search" checked> Use LLM search (validators)
|
| 305 |
+
</label>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
<div class="control-group" style="margin-top: 10px;" id="manual-chunk-group" hidden>
|
| 309 |
+
<label><strong>Manual Localization(s):</strong> (Include both prompt and completion)</label>
|
| 310 |
+
<textarea id="manual-chunks" rows="3" style="width: 100%; font-family: monospace;"></textarea>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<div class="control-group" style="margin-top: 10px;">
|
| 314 |
+
<label><strong>Validator Models:</strong></label>
|
| 315 |
+
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin: 10px 0;">
|
| 316 |
+
<label><input type="checkbox" id="validator-gpt52" checked> GPT-5.2</label>
|
| 317 |
+
<label><input type="checkbox" id="validator-gptoss" checked> GPT-OSS-120B</label>
|
| 318 |
+
<label><input type="checkbox" id="validator-deepseek" checked> DeepSeek-V3.2</label>
|
| 319 |
+
<label><input type="checkbox" id="validator-kimi"> Kimi-K2-Thinking</label>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
|
| 323 |
+
<div class="action-buttons">
|
| 324 |
+
<button class="btn-primary" id="btn-locate-error">Locate Target Token</button>
|
| 325 |
+
<span id="error-locate-status" style="margin-left: 15px; line-height: 40px; color: #666;"></span>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
<!-- Results -->
|
| 329 |
+
<div id="error-location-results" class="hidden" style="margin-top: 20px;">
|
| 330 |
+
<h5 style="color: #667eea;">📊 Voting Results:</h5>
|
| 331 |
+
<div id="vote-details" class="vote-details">
|
| 332 |
+
<!-- Populated by JS -->
|
| 333 |
+
</div>
|
| 334 |
+
|
| 335 |
+
<h5 style="color: #667eea; margin-top: 15px;">✂️ Truncated Text:</h5>
|
| 336 |
+
<div class="control-group">
|
| 337 |
+
<textarea id="truncated-text" rows="5" style="width: 100%; font-family: monospace;"></textarea>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<h5 style="color: #667eea;">💡 Explanation:</h5>
|
| 341 |
+
<div id="error-explanation" style="padding: 10px; background: #fff3cd; border-radius: 5px; white-space: pre-wrap;"></div>
|
| 342 |
+
|
| 343 |
+
<div class="action-buttons" style="margin-top: 15px;">
|
| 344 |
+
<button class="btn-primary" id="btn-accept-truncation">Accept & Use Truncated Text</button>
|
| 345 |
+
<button class="btn-secondary" id="btn-edit-truncation">Edit Truncation</button>
|
| 346 |
+
<span id="accept-status" style="margin-left: 15px; line-height: 40px; color: #28a745; font-weight: bold;"></span>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
|
| 351 |
+
<!-- Subsection 3.2: Contrast Token Selection -->
|
| 352 |
+
<div class="subsection" id="contrast-selection-section" class="hidden">
|
| 353 |
+
<h4>🔄 Contrast Token Selection</h4>
|
| 354 |
+
<p style="color: #666; margin-bottom: 15px;">
|
| 355 |
+
After error token localization, identify the contrast token (correct alternative).
|
| 356 |
+
</p>
|
| 357 |
+
|
| 358 |
+
<div class="control-group">
|
| 359 |
+
<label><strong>Selection Method:</strong></label>
|
| 360 |
+
<div style="margin: 10px 0;">
|
| 361 |
+
<label style="display: block; margin: 5px 0;">
|
| 362 |
+
<input type="radio" name="contrast-method" value="manual" checked>
|
| 363 |
+
Manual Selection (from Top-K predictions)
|
| 364 |
+
</label>
|
| 365 |
+
<label style="display: block; margin: 5px 0;">
|
| 366 |
+
<input type="radio" name="contrast-method" value="beam">
|
| 367 |
+
Beam Search (automatic exploration)
|
| 368 |
+
</label>
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
+
|
| 372 |
+
<div id="manual-contrast-section">
|
| 373 |
+
<div class="action-buttons">
|
| 374 |
+
<button class="btn-primary" id="btn-compute-topk">Compute Top-K Predictions</button>
|
| 375 |
+
<label style="margin-left: 15px;">
|
| 376 |
+
K = <input type="number" id="topk-value" value="10" min="1" max="100" style="width: 60px;">
|
| 377 |
+
</label>
|
| 378 |
+
</div>
|
| 379 |
+
|
| 380 |
+
<!-- Top-K Table -->
|
| 381 |
+
<div id="topk-results" class="hidden" style="margin-top: 20px;">
|
| 382 |
+
<h5>Top-K Token Predictions:</h5>
|
| 383 |
+
<div class="scrollable-table">
|
| 384 |
+
<table id="topk-table">
|
| 385 |
+
<thead>
|
| 386 |
+
<tr>
|
| 387 |
+
<th>Rank</th>
|
| 388 |
+
<th>Token</th>
|
| 389 |
+
<th>Logit</th>
|
| 390 |
+
<th>Action</th>
|
| 391 |
+
</tr>
|
| 392 |
+
</thead>
|
| 393 |
+
<tbody>
|
| 394 |
+
<!-- Populated by JS -->
|
| 395 |
+
</tbody>
|
| 396 |
+
</table>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
|
| 401 |
+
<div id="beam-contrast-section" class="hidden">
|
| 402 |
+
<p style="color: #999; font-style: italic;">Beam search functionality - Coming soon...</p>
|
| 403 |
+
<div class="action-buttons">
|
| 404 |
+
<button class="btn-primary" disabled>Run Beam Search</button>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
|
| 408 |
+
<!-- Selected Contrast Token Display -->
|
| 409 |
+
<div id="contrast-selected" class="hidden" style="margin-top: 20px; padding: 15px; background: #d4edda; border-radius: 5px;">
|
| 410 |
+
<h5 style="margin-top: 0; color: #155724;">✓ Contrast Token Pair Identified:</h5>
|
| 411 |
+
<p><strong>Error Token:</strong> <span id="display-error-token">-</span></p>
|
| 412 |
+
<p><strong>Contrast Token:</strong> <span id="display-contrast-token">-</span></p>
|
| 413 |
+
<p><strong>Logit Difference:</strong> <span id="display-logit-diff">-</span></p>
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
|
| 417 |
+
<div class="action-buttons" style="margin-top: 20px;">
|
| 418 |
+
<button class="btn-primary" id="btn-proceed-to-lrp" disabled>Proceed to Input Attribution Analysis</button>
|
| 419 |
+
</div>
|
| 420 |
+
</div>
|
| 421 |
+
</div>
|
| 422 |
+
|
| 423 |
+
<!-- Step 3: Input Attribution -->
|
| 424 |
+
<div class="step-container" id="step-3">
|
| 425 |
+
<div class="step-header disabled" onclick="toggleStep(3)">
|
| 426 |
+
<div style="display: flex; align-items: center;">
|
| 427 |
+
<span class="step-number">3</span>
|
| 428 |
+
<span class="step-title">Input Attribution Analysis</span>
|
| 429 |
+
</div>
|
| 430 |
+
<span class="step-status" id="status-3">Locked</span>
|
| 431 |
+
</div>
|
| 432 |
+
<div class="step-body collapsed" id="body-3">
|
| 433 |
+
|
| 434 |
+
<div class="subsection attnlrp-only">
|
| 435 |
+
<h4>⚙️ Input Attribution Configuration</h4>
|
| 436 |
+
<div class="control-group">
|
| 437 |
+
<label>Rule:
|
| 438 |
+
<select id="lrp-rule">
|
| 439 |
+
<option value="Attn-LRP" selected>Attn-LRP</option>
|
| 440 |
+
<option value="CP-LRP">CP-LRP</option>
|
| 441 |
+
<option value="Gradient">Gradient (Vanilla)</option>
|
| 442 |
+
</select>
|
| 443 |
+
</label>
|
| 444 |
+
<label style="margin-left: 20px;">
|
| 445 |
+
<input type="checkbox" id="capture-mid"> Attn/MLP Split
|
| 446 |
+
</label>
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
<div class="subsection nnsight-only">
|
| 451 |
+
<h4>⚙️ NNsight Attribution Configuration</h4>
|
| 452 |
+
<p style="color: #666;">NNsight uses neural network surgery to trace attributions through the model's computation graph.</p>
|
| 453 |
+
</div>
|
| 454 |
+
|
| 455 |
+
<div class="subsection pyvene-only">
|
| 456 |
+
<h4>⚙️ Pyvene Attribution Configuration</h4>
|
| 457 |
+
<p style="color: #666;">Pyvene uses causal interventions to identify token attributions.</p>
|
| 458 |
+
</div>
|
| 459 |
+
|
| 460 |
+
<div class="subsection">
|
| 461 |
+
<h4>🎨 Input Attribution Visualization</h4>
|
| 462 |
+
<div class="control-group">
|
| 463 |
+
<label>Mode:
|
| 464 |
+
<select id="attr-mode">
|
| 465 |
+
<option value="max_logit">Max Logit</option>
|
| 466 |
+
<option value="logit_diff" selected>Logit Difference</option>
|
| 467 |
+
</select>
|
| 468 |
+
</label>
|
| 469 |
+
<label style="margin-left: 15px;">Strategy:
|
| 470 |
+
<select id="attr-strategy">
|
| 471 |
+
<option value="by_topk_avg">By Top-K Avg</option>
|
| 472 |
+
<option value="demean">Demean</option>
|
| 473 |
+
<option value="by_ref_token" selected>By Reference Token</option>
|
| 474 |
+
</select>
|
| 475 |
+
</label>
|
| 476 |
+
</div>
|
| 477 |
+
|
| 478 |
+
<div class="action-buttons">
|
| 479 |
+
<button class="btn-primary" id="btn-compute-attribution">Compute Input Attribution</button>
|
| 480 |
+
</div>
|
| 481 |
+
|
| 482 |
+
<div id="attribution-results" class="hidden" style="margin-top: 20px;">
|
| 483 |
+
<h5>Attribution Map:</h5>
|
| 484 |
+
<div id="attribution-display" style="padding: 15px; background: white; border: 1px solid #ddd; border-radius: 5px; font-family: monospace; line-height: 1.8;">
|
| 485 |
+
<!-- Populated by JS -->
|
| 486 |
+
</div>
|
| 487 |
+
<div class="action-buttons" style="margin-top: 10px;">
|
| 488 |
+
<button class="btn-secondary" id="btn-save-attr-png" style="background-color: #2ecc71;">Save PNG</button>
|
| 489 |
+
<button class="btn-secondary" id="btn-save-attr-pdf" style="background-color: #e74c3c;">Save PDF</button>
|
| 490 |
+
</div>
|
| 491 |
+
<!-- Positive Relevance Sorted Indices -->
|
| 492 |
+
<div id="positive-relevance-sorted" class="hidden" style="margin-top: 18px;">
|
| 493 |
+
<h5 style="margin: 0 0 8px 0; color: #667eea;">Positive Relevance Tokens (Sorted by Relevance, Descending)</h5>
|
| 494 |
+
<div id="pos-rel-indices" style="padding: 10px; background: #f8f9fa; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 0.95em; word-break: break-all; line-height: 1.8;"></div>
|
| 495 |
+
<h5 style="margin: 12px 0 8px 0; color: #7f8c8d; font-size: 0.9em;">Corresponding Tokens (for verification)</h5>
|
| 496 |
+
<div id="pos-rel-tokens" style="padding: 10px; background: #fefefe; border: 1px solid #eee; border-radius: 4px; font-family: monospace; font-size: 0.9em; word-break: break-all; line-height: 1.8; color: #555;"></div>
|
| 497 |
+
</div>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
<!-- Perturbation Evaluation Subsection -->
|
| 502 |
+
<div class="subsection">
|
| 503 |
+
<h4>🧪 Perturbation Evaluation (Ground-Truth Comparison)</h4>
|
| 504 |
+
<p style="color: #666; margin-bottom: 15px;">
|
| 505 |
+
Evaluate attribution quality by zeroing out top-attributed tokens and checking if the model's error is fixed.
|
| 506 |
+
A better attribution method should fix the error at smaller K values.
|
| 507 |
+
</p>
|
| 508 |
+
<div class="control-group">
|
| 509 |
+
<label>K values (comma-separated):
|
| 510 |
+
<input type="text" id="perturbation-k-values" value="1, 3, 5, 10" style="width: 200px;">
|
| 511 |
+
</label>
|
| 512 |
+
</div>
|
| 513 |
+
<div class="action-buttons">
|
| 514 |
+
<button class="btn-primary" id="btn-run-perturbation" disabled>Run Perturbation Evaluation</button>
|
| 515 |
+
<span id="perturbation-status" style="margin-left: 15px; line-height: 40px; color: #666;"></span>
|
| 516 |
+
</div>
|
| 517 |
+
<div id="perturbation-results" class="hidden" style="margin-top: 20px;">
|
| 518 |
+
<h5>Perturbation Results:</h5>
|
| 519 |
+
<div class="scrollable-table">
|
| 520 |
+
<table id="perturbation-table">
|
| 521 |
+
<thead>
|
| 522 |
+
<tr>
|
| 523 |
+
<th>K</th>
|
| 524 |
+
<th>Perturbed Tokens</th>
|
| 525 |
+
<th>New Top-1 Token</th>
|
| 526 |
+
<th>Error Fixed?</th>
|
| 527 |
+
<th>Target Rank After</th>
|
| 528 |
+
<th>Logit Change</th>
|
| 529 |
+
</tr>
|
| 530 |
+
</thead>
|
| 531 |
+
<tbody>
|
| 532 |
+
<!-- Populated by JS -->
|
| 533 |
+
</tbody>
|
| 534 |
+
</table>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
|
| 538 |
+
<!-- Manual Perturbation Sub-section -->
|
| 539 |
+
<div style="margin-top: 25px; padding-top: 20px; border-top: 2px dashed #dee2e6;">
|
| 540 |
+
<h5 style="color: #667eea; margin-top: 0;">🖱️ Manual Token Perturbation</h5>
|
| 541 |
+
<p style="color: #666; font-size: 0.9em; margin-bottom: 12px;">
|
| 542 |
+
Click tokens in the <strong>Attribution Map</strong> above to select/deselect which tokens to mask.
|
| 543 |
+
Selected tokens will be highlighted with a dashed border.
|
| 544 |
+
</p>
|
| 545 |
+
<div style="margin-bottom: 12px;">
|
| 546 |
+
<strong style="font-size: 0.9em;">Selected Tokens:</strong>
|
| 547 |
+
<div id="manual-perturb-selected" style="display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; min-height: 32px; padding: 8px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px;">
|
| 548 |
+
<span style="color: #999; font-size: 0.85em; line-height: 24px;" id="manual-perturb-placeholder">No tokens selected</span>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
<div class="action-buttons">
|
| 552 |
+
<button class="btn-secondary" id="btn-clear-manual-selection" style="background: #95a5a6;">Clear Selection</button>
|
| 553 |
+
<button class="btn-primary" id="btn-run-manual-perturbation" disabled>Run Manual Perturbation</button>
|
| 554 |
+
<span id="manual-perturbation-status" style="margin-left: 15px; line-height: 40px; color: #666;"></span>
|
| 555 |
+
</div>
|
| 556 |
+
<div id="manual-perturbation-result" class="hidden" style="margin-top: 15px;">
|
| 557 |
+
<!-- Populated by JS -->
|
| 558 |
+
</div>
|
| 559 |
+
</div>
|
| 560 |
+
</div>
|
| 561 |
+
|
| 562 |
+
<div class="action-buttons" style="margin-top: 20px;">
|
| 563 |
+
<button class="btn-primary" id="btn-proceed-to-graph">Proceed to Attribution Graph</button>
|
| 564 |
+
</div>
|
| 565 |
+
</div>
|
| 566 |
+
</div>
|
| 567 |
+
|
| 568 |
+
<!-- Step 4: Attribution Graph -->
|
| 569 |
+
<div class="step-container" id="step-4">
|
| 570 |
+
<div class="step-header disabled" onclick="toggleStep(4)">
|
| 571 |
+
<div style="display: flex; align-items: center;">
|
| 572 |
+
<span class="step-number">4</span>
|
| 573 |
+
<span class="step-title">Attribution Graph Visualization</span>
|
| 574 |
+
</div>
|
| 575 |
+
<span class="step-status" id="status-4">Locked</span>
|
| 576 |
+
</div>
|
| 577 |
+
<div class="step-body collapsed" id="body-4">
|
| 578 |
+
|
| 579 |
+
<div class="subsection">
|
| 580 |
+
<h4>🔧 Graph Configuration</h4>
|
| 581 |
+
|
| 582 |
+
<div class="control-group">
|
| 583 |
+
<label><strong>Layer Selection:</strong></label>
|
| 584 |
+
<input type="text" id="layers-list" placeholder="e.g., 0, 10, 20, 27" style="width: 300px;">
|
| 585 |
+
<button class="btn-secondary" id="btn-layer-preset-default">Recommended</button>
|
| 586 |
+
<button class="btn-secondary" id="btn-layer-preset-all">All Layers</button>
|
| 587 |
+
</div>
|
| 588 |
+
|
| 589 |
+
<div class="control-group" style="margin-top: 15px;">
|
| 590 |
+
<label>Backprop Mode:
|
| 591 |
+
<select id="graph-bp-mode">
|
| 592 |
+
<option value="max_logit">Max Logit</option>
|
| 593 |
+
<option value="logit_diff" selected>Logit Difference</option>
|
| 594 |
+
</select>
|
| 595 |
+
</label>
|
| 596 |
+
<label style="margin-left: 15px;">Strategy:
|
| 597 |
+
<select id="graph-bp-strategy">
|
| 598 |
+
<option value="by_topk_avg" selected>By Top-K Avg</option>
|
| 599 |
+
<option value="demean">Demean</option>
|
| 600 |
+
<option value="by_ref_token">By Reference Token</option>
|
| 601 |
+
</select>
|
| 602 |
+
</label>
|
| 603 |
+
</div>
|
| 604 |
+
|
| 605 |
+
<div class="control-group" style="margin-top: 15px;">
|
| 606 |
+
<label>Pruning Mode:
|
| 607 |
+
<select id="pruning-mode">
|
| 608 |
+
<option value="by_per_layer_cum_mass_percentile" selected>By Top-P Mass (Layer)</option>
|
| 609 |
+
<option value="by_global_threshold">By Global Threshold</option>
|
| 610 |
+
</select>
|
| 611 |
+
</label>
|
| 612 |
+
</div>
|
| 613 |
+
|
| 614 |
+
<div id="ctrl-top-p" class="control-group" style="margin-top: 10px;">
|
| 615 |
+
<label>Top-P Mass (0-1):
|
| 616 |
+
<input type="range" id="top-p-slider" min="0" max="1" step="0.01" value="0.85" style="width: 200px; margin-left: 10px;">
|
| 617 |
+
<input type="number" id="top-p-value" value="0.85" min="0" max="1" step="0.01" style="width: 70px; margin-left: 10px;">
|
| 618 |
+
</label>
|
| 619 |
+
</div>
|
| 620 |
+
|
| 621 |
+
<div id="ctrl-global-thresh" class="control-group hidden" style="margin-top: 10px;">
|
| 622 |
+
<label>Global Edge Threshold:
|
| 623 |
+
<input type="number" id="global-thresh-value" value="0.01" min="0" max="1" step="0.001" style="width: 100px; margin-left: 10px;">
|
| 624 |
+
</label>
|
| 625 |
+
</div>
|
| 626 |
+
|
| 627 |
+
<div class="action-buttons">
|
| 628 |
+
<button class="btn-primary" id="btn-compute-graph">Compute & Visualize Graph</button>
|
| 629 |
+
</div>
|
| 630 |
+
</div>
|
| 631 |
+
|
| 632 |
+
<div id="graph-results" class="hidden" style="margin-top: 20px;">
|
| 633 |
+
<h4>📊 Circuit Visualization</h4>
|
| 634 |
+
|
| 635 |
+
<!-- Visualization Controls -->
|
| 636 |
+
<div style="display: flex; gap: 15px; margin-bottom: 15px; flex-wrap: wrap; align-items: center;">
|
| 637 |
+
<label><input type="checkbox" id="show-all-tokens"> Show All Tokens</label>
|
| 638 |
+
<label><input type="checkbox" id="hide-bos"> Hide BOS Token</label>
|
| 639 |
+
<label><input type="checkbox" id="show-values" checked> Show Node Values</label>
|
| 640 |
+
</div>
|
| 641 |
+
|
| 642 |
+
<div style="display: flex; gap: 20px; margin-bottom: 15px; flex-wrap: wrap; align-items: center;">
|
| 643 |
+
<label style="display: flex; align-items: center; gap: 8px;">
|
| 644 |
+
Line Strength:
|
| 645 |
+
<input type="range" id="line-strength" min="2" max="20" value="6" step="0.1" style="width: 120px;">
|
| 646 |
+
<span id="line-strength-val" style="min-width: 35px; font-weight: bold;">6.0</span>
|
| 647 |
+
</label>
|
| 648 |
+
<label style="display: flex; align-items: center; gap: 8px;">
|
| 649 |
+
Layer Spacing (px):
|
| 650 |
+
<input type="range" id="layer-spacing-slider" min="50" max="400" value="100" step="10" style="width: 120px;">
|
| 651 |
+
<input type="number" id="layer-spacing-value" min="50" max="400" value="100" step="10" style="width: 60px;">
|
| 652 |
+
</label>
|
| 653 |
+
</div>
|
| 654 |
+
|
| 655 |
+
<!-- Threshold Adjustment (Post-Compute) -->
|
| 656 |
+
<div style="padding: 10px; background: #f0f0f0; border-radius: 5px; margin-bottom: 15px;">
|
| 657 |
+
<strong>🎛️ Adjust Graph Sparsity:</strong>
|
| 658 |
+
<div id="post-ctrl-top-p" style="margin-top: 8px;">
|
| 659 |
+
<label>Top-P Mass:
|
| 660 |
+
<input type="range" id="post-top-p-slider" min="0" max="1" step="0.01" value="0.85" style="width: 200px; margin-left: 10px;">
|
| 661 |
+
<input type="number" id="post-top-p-value" value="0.85" min="0" max="1" step="0.01" style="width: 70px; margin-left: 10px;">
|
| 662 |
+
<button class="btn-secondary" id="btn-recompute-threshold" style="margin-left: 10px;">Recompute</button>
|
| 663 |
+
</label>
|
| 664 |
+
</div>
|
| 665 |
+
<div id="post-ctrl-global-thresh" class="hidden" style="margin-top: 8px;">
|
| 666 |
+
<label>Global Threshold:
|
| 667 |
+
<input type="number" id="post-global-thresh-value" value="0.01" min="0" max="1" step="0.001" style="width: 100px; margin-left: 10px;">
|
| 668 |
+
<button class="btn-secondary" id="btn-recompute-threshold-global" style="margin-left: 10px;">Recompute</button>
|
| 669 |
+
</label>
|
| 670 |
+
</div>
|
| 671 |
+
</div>
|
| 672 |
+
|
| 673 |
+
<div id="canvas-container" style="border: 1px solid #ddd; border-radius: 5px; overflow: hidden;">
|
| 674 |
+
<canvas id="graph-canvas"></canvas>
|
| 675 |
+
<div id="graph-tooltip"></div>
|
| 676 |
+
</div>
|
| 677 |
+
|
| 678 |
+
<div class="action-buttons" style="margin-top: 15px;">
|
| 679 |
+
<button class="btn-secondary" id="btn-save-graph">Save as PNG</button>
|
| 680 |
+
<button class="btn-secondary" id="btn-save-graph-pdf">Save as PDF</button>
|
| 681 |
+
</div>
|
| 682 |
+
</div>
|
| 683 |
+
|
| 684 |
+
<div style="margin-top: 30px; padding: 20px; background: #d4edda; border-radius: 8px;">
|
| 685 |
+
<h3 style="margin-top: 0; color: #155724;">✅ Analysis Complete!</h3>
|
| 686 |
+
<p>You have completed the full step-by-step analysis pipeline. Review the results above or export your findings.</p>
|
| 687 |
+
</div>
|
| 688 |
+
</div>
|
| 689 |
+
</div>
|
| 690 |
+
|
| 691 |
+
</main>
|
| 692 |
+
</div>
|
| 693 |
+
|
| 694 |
+
<!-- Loading Overlay -->
|
| 695 |
+
<div id="loading-overlay" class="hidden">
|
| 696 |
+
<div class="spinner"></div>
|
| 697 |
+
<div id="loading-message">Processing...</div>
|
| 698 |
+
</div>
|
| 699 |
+
|
| 700 |
+
<script src="js/main_new.js?v=20260330"></script>
|
| 701 |
+
</body>
|
| 702 |
+
</html>
|
frontend/index_old.html
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>LLM Insider for Failure Debugging</title>
|
| 7 |
+
<link rel="stylesheet" href="css/style.css">
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
| 9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div id="app">
|
| 13 |
+
<header>
|
| 14 |
+
<h1>LLM Insider for Failure Debugging</h1>
|
| 15 |
+
</header>
|
| 16 |
+
|
| 17 |
+
<main>
|
| 18 |
+
<!-- Section 1: Build Circuit (Model & Inputs) -->
|
| 19 |
+
<section id="section-setup" class="panel">
|
| 20 |
+
<h2>1. Setup & Inputs</h2>
|
| 21 |
+
|
| 22 |
+
<!-- Trace Loading (Top) -->
|
| 23 |
+
<div class="control-group" style="background: #f8f9fa; padding: 10px; border-radius: 4px; border: 1px dashed #ced4da;">
|
| 24 |
+
<label><strong>Load from Trace:</strong></label>
|
| 25 |
+
<label>Dataset:
|
| 26 |
+
<select id="trace-dataset">
|
| 27 |
+
<option value="">-- Select --</option>
|
| 28 |
+
<!-- Populated by JS -->
|
| 29 |
+
</select>
|
| 30 |
+
</label>
|
| 31 |
+
<label>Trace:
|
| 32 |
+
<select id="trace-file" disabled>
|
| 33 |
+
<option value="">-- Select Dataset First --</option>
|
| 34 |
+
</select>
|
| 35 |
+
</label>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div class="control-group">
|
| 39 |
+
<div style="display: flex; flex-direction: column; gap: 5px;">
|
| 40 |
+
<div style="display: flex; gap: 10px; align-items: center;">
|
| 41 |
+
<label>Series:
|
| 42 |
+
<select id="model-series">
|
| 43 |
+
<option value="Qwen3">Qwen3</option>
|
| 44 |
+
<option value="Qwen2">Qwen2</option>
|
| 45 |
+
<option value="OLMo3">OLMo3</option>
|
| 46 |
+
</select>
|
| 47 |
+
</label>
|
| 48 |
+
<label>Model:
|
| 49 |
+
<select id="model-select">
|
| 50 |
+
<!-- Populated dynamically -->
|
| 51 |
+
</select>
|
| 52 |
+
</label>
|
| 53 |
+
</div>
|
| 54 |
+
<div style="display: flex; gap: 10px; align-items: center;">
|
| 55 |
+
<label>Revision:
|
| 56 |
+
<select id="model-revision" disabled>
|
| 57 |
+
<option value="">Latest (Default)</option>
|
| 58 |
+
</select>
|
| 59 |
+
</label>
|
| 60 |
+
<label>Path:
|
| 61 |
+
<input type="text" id="model-path" value="Qwen/Qwen3-0.6B" style="width: 250px;">
|
| 62 |
+
</label>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<label>DType:
|
| 67 |
+
<select id="model-dtype">
|
| 68 |
+
<option value="float16">float16</option>
|
| 69 |
+
<option value="bfloat16" selected>bfloat16</option>
|
| 70 |
+
<option value="float32">float32</option>
|
| 71 |
+
<option value="auto">auto</option>
|
| 72 |
+
</select>
|
| 73 |
+
</label>
|
| 74 |
+
<label>LRP Rule:
|
| 75 |
+
<select id="model-lrp-rule">
|
| 76 |
+
<option value="Attn-LRP" selected>Attn-LRP</option>
|
| 77 |
+
<option value="CP-LRP">CP-LRP</option>
|
| 78 |
+
</select>
|
| 79 |
+
</label>
|
| 80 |
+
<label>
|
| 81 |
+
<input type="checkbox" id="quant-4bit"> 4-bit Quantization
|
| 82 |
+
</label>
|
| 83 |
+
<button id="btn-load-model">Load Model</button>
|
| 84 |
+
<span id="model-status" class="status">Not Loaded</span>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="control-group">
|
| 88 |
+
<label for="prompt-input">Prompt:</label>
|
| 89 |
+
<textarea id="prompt-input" rows="3">After John and Mary went to the store, John gave a bottle of milk to</textarea>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<div class="control-group">
|
| 93 |
+
<label for="completion-input">Completion:</label>
|
| 94 |
+
<textarea id="completion-input" rows="3" placeholder="Enter the completion text here..."></textarea>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div class="control-group" style="display: flex; gap: 10px; align-items: center;">
|
| 98 |
+
<button id="btn-locate-error" disabled style="background-color: #e74c3c;">Locate Error Token</button>
|
| 99 |
+
<span id="error-locate-status" style="color: #666;"></span>
|
| 100 |
+
</div>
|
| 101 |
+
</section>
|
| 102 |
+
|
| 103 |
+
<!-- Section: Error Token Location Result -->
|
| 104 |
+
<section id="section-error-result" class="panel hidden">
|
| 105 |
+
<h2>Error Token Location Result</h2>
|
| 106 |
+
<div class="control-group">
|
| 107 |
+
<label for="error-explanation"><strong>Error Analysis:</strong></label>
|
| 108 |
+
<div id="error-explanation" style="padding: 10px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; margin-bottom: 10px; white-space: pre-wrap;"></div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div class="control-group">
|
| 112 |
+
<label for="truncated-text"><strong>Truncated Text (Editable):</strong></label>
|
| 113 |
+
<textarea id="truncated-text" rows="5" style="width: 100%; font-family: monospace;"></textarea>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div class="control-group" style="display: flex; gap: 10px;">
|
| 117 |
+
<button id="btn-use-truncated" style="background-color: #28a745;">Use This Text</button>
|
| 118 |
+
<button id="btn-cancel-error" style="background-color: #6c757d;">Cancel</button>
|
| 119 |
+
</div>
|
| 120 |
+
</section>
|
| 121 |
+
|
| 122 |
+
<!-- Section for continuing analysis -->
|
| 123 |
+
<section id="section-continue-analysis" class="panel hidden">
|
| 124 |
+
<h2>Continue Analysis</h2>
|
| 125 |
+
<div class="control-group">
|
| 126 |
+
<label for="prompt-orig-completion">Current Text for Analysis:</label>
|
| 127 |
+
<textarea id="prompt-orig-completion" rows="3" readonly style="background-color: #f8f9fa;"></textarea>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<div class="control-group">
|
| 131 |
+
<label style="margin-right: 15px;">
|
| 132 |
+
<input type="checkbox" id="append-bos"> Append BOS Token
|
| 133 |
+
</label>
|
| 134 |
+
<label style="margin-right: 15px;">
|
| 135 |
+
<input type="checkbox" id="capture-mid"> Attn/MLP Split
|
| 136 |
+
</label>
|
| 137 |
+
<button id="btn-compute-logits" disabled>Compute Logits</button>
|
| 138 |
+
<span id="token-count-display" style="margin-left: 15px; color: #666;"></span>
|
| 139 |
+
</div>
|
| 140 |
+
</section>
|
| 141 |
+
|
| 142 |
+
<!-- Section: Trace Exploration Data (Hidden by default, shown when trace loaded) -->
|
| 143 |
+
<section id="section-trace-explore" class="panel hidden">
|
| 144 |
+
<details>
|
| 145 |
+
<summary style="cursor: pointer; font-weight: bold; font-size: 1.2em; margin-bottom: 10px;">Trace Candidate Exploration (Correctness Check)</summary>
|
| 146 |
+
<div style="display: flex; gap: 20px; flex-wrap: wrap; margin-top: 10px;">
|
| 147 |
+
<div style="flex: 1; min-width: 400px;">
|
| 148 |
+
<h3 style="margin-top: 0;">Current Model Candidates</h3>
|
| 149 |
+
<div id="container-explore-original" style="max-height: 400px; overflow-y: auto; border: 1px solid #ccc; background: white;">
|
| 150 |
+
<!-- Table will be injected here -->
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
<div style="flex: 1; min-width: 400px;">
|
| 154 |
+
<h3 id="header-explore-other" style="margin-top: 0;">Other Model Candidates</h3>
|
| 155 |
+
<div id="container-explore-4b" style="max-height: 400px; overflow-y: auto; border: 1px solid #ccc; background: white;">
|
| 156 |
+
<!-- Table will be injected here -->
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
</details>
|
| 161 |
+
</section>
|
| 162 |
+
|
| 163 |
+
<!-- Section 1 Output: Logits -->
|
| 164 |
+
<section id="section-logits" class="panel hidden">
|
| 165 |
+
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
|
| 166 |
+
<h2 style="margin: 0;">Logit Analysis (Top-K)</h2>
|
| 167 |
+
<div style="display: flex; gap: 10px; align-items: center;">
|
| 168 |
+
<input type="text" id="add-token-input" placeholder="Token Str or ID" style="padding: 4px; border: 1px solid #ccc; border-radius: 4px; width: 120px;">
|
| 169 |
+
<button id="btn-add-token" style="padding: 4px 8px; font-size: 0.9em;">Add Token</button>
|
| 170 |
+
<span style="border-left: 1px solid #ccc; height: 20px; margin: 0 5px;"></span>
|
| 171 |
+
<label style="font-size: 0.9rem;">Gen Length: <input type="number" id="gen-max-tokens" value="30" min="1" max="200" style="width: 50px; padding: 4px; border: 1px solid #ccc; border-radius: 4px;"></label>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
<!-- Table -->
|
| 175 |
+
<div class="scrollable-table" style="margin-top: 10px;">
|
| 176 |
+
<table id="logits-table">
|
| 177 |
+
<thead>
|
| 178 |
+
<tr>
|
| 179 |
+
<th>Rank</th>
|
| 180 |
+
<th>Token ID</th>
|
| 181 |
+
<th>Token</th>
|
| 182 |
+
<th>Logit</th>
|
| 183 |
+
<th>Action</th>
|
| 184 |
+
<th>Continual Generation</th>
|
| 185 |
+
</tr>
|
| 186 |
+
</thead>
|
| 187 |
+
<tbody>
|
| 188 |
+
<!-- Populated by JS -->
|
| 189 |
+
</tbody>
|
| 190 |
+
</table>
|
| 191 |
+
</div>
|
| 192 |
+
<!-- Selection State for Circuit -->
|
| 193 |
+
<div id="circuit-selection-info">
|
| 194 |
+
<p><strong>Top Predicted:</strong> <span id="top-token-display">-</span></p>
|
| 195 |
+
<p><strong>Contrast With:</strong> <span id="contrast-token-display">None</span></p>
|
| 196 |
+
</div>
|
| 197 |
+
</section>
|
| 198 |
+
|
| 199 |
+
<!-- Section 1.5: Input Attribution -->
|
| 200 |
+
<section id="section-input-attr" class="panel hidden">
|
| 201 |
+
<h2>2. Input Attribution Visualization</h2>
|
| 202 |
+
<div class="controls-row">
|
| 203 |
+
<div class="control-col">
|
| 204 |
+
<h3>Attribution Config</h3>
|
| 205 |
+
<label>Mode:
|
| 206 |
+
<select id="attr-bp-mode">
|
| 207 |
+
<option value="max_logit">Max Logit</option>
|
| 208 |
+
<option value="logit_diff" selected>Logit Difference</option>
|
| 209 |
+
</select>
|
| 210 |
+
</label>
|
| 211 |
+
<div id="attr-diff-strategy-opts">
|
| 212 |
+
<label>Strategy:
|
| 213 |
+
<select id="attr-bp-strategy">
|
| 214 |
+
<option value="by_topk_avg">By Top-K Avg</option>
|
| 215 |
+
<option value="demean">Demean (Avg All)</option>
|
| 216 |
+
<option value="by_ref_token" selected>By Reference Token</option>
|
| 217 |
+
</select>
|
| 218 |
+
</label>
|
| 219 |
+
<label id="attr-opt-ref">Ref ID: <input type="number" id="attr-bp-ref-id" placeholder="ID"></label>
|
| 220 |
+
</div>
|
| 221 |
+
<label style="margin-top: 10px; display: block; font-size: 0.9em;">
|
| 222 |
+
<input type="checkbox" id="attr-hide-bos" checked> Hide First Token
|
| 223 |
+
</label>
|
| 224 |
+
<label style="margin-top: 5px; display: block; font-size: 0.9em;">
|
| 225 |
+
<input type="checkbox" id="attr-hide-special" checked> Hide Special Tokens (<|im_start|>, <think>, etc.)
|
| 226 |
+
</label>
|
| 227 |
+
<label style="margin-top: 5px; display: block; font-size: 0.9em;">
|
| 228 |
+
<input type="checkbox" id="attr-reverse-sign"> Reverse Sign (Red ↔ Blue)
|
| 229 |
+
</label>
|
| 230 |
+
<button id="btn-compute-attr">Compute Attribution</button>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="control-col" style="flex: 2;">
|
| 233 |
+
<h3>Attribution Map</h3>
|
| 234 |
+
<div id="input-attribution-display" class="attribution-text-box">
|
| 235 |
+
<!-- Tokens will be rendered here -->
|
| 236 |
+
Click "Compute Attribution" to see results.
|
| 237 |
+
</div>
|
| 238 |
+
<div style="margin-top: 10px; display: flex; gap: 10px;">
|
| 239 |
+
<button id="btn-save-attr-png" style="background-color: #2ecc71; font-size: 0.9em; padding: 4px 10px;">Save PNG</button>
|
| 240 |
+
<button id="btn-save-attr-pdf" style="background-color: #e74c3c; font-size: 0.9em; padding: 4px 10px;">Save PDF</button>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
</section>
|
| 245 |
+
|
| 246 |
+
<!-- Section 2: Circuit Visualization -->
|
| 247 |
+
<section id="section-circuit" class="panel hidden">
|
| 248 |
+
<h2>3. Circuit Visualization</h2>
|
| 249 |
+
|
| 250 |
+
<!-- PROGRESS BAR CONTAINER -->
|
| 251 |
+
<div id="compute-progress-container" style="display: none; margin-bottom: 1rem; border: 1px solid #ddd; padding: 10px; border-radius: 4px; background: #f0f8ff;">
|
| 252 |
+
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
| 253 |
+
<strong id="progress-status-text">Computation Status...</strong>
|
| 254 |
+
<span id="progress-percent-text">0%</span>
|
| 255 |
+
</div>
|
| 256 |
+
<progress id="compute-progress-bar" value="0" max="100" style="width: 100%; height: 20px;"></progress>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
<div class="controls-row">
|
| 260 |
+
<!-- Column 1: Backprop Config -->
|
| 261 |
+
<div class="control-col">
|
| 262 |
+
<h3>Backprop Strategy</h3>
|
| 263 |
+
<label>Mode:
|
| 264 |
+
<select id="bp-mode">
|
| 265 |
+
<option value="max_logit">Max Logit (Target Only)</option>
|
| 266 |
+
<option value="logit_diff" selected>Logit Difference</option>
|
| 267 |
+
</select>
|
| 268 |
+
</label>
|
| 269 |
+
|
| 270 |
+
<div id="diff-strategy-opts">
|
| 271 |
+
<label>Strategy:
|
| 272 |
+
<select id="bp-strategy">
|
| 273 |
+
<option value="by_topk_avg" selected>By Top-K Avg</option>
|
| 274 |
+
<option value="demean">Demean (Avg All)</option>
|
| 275 |
+
<option value="by_ref_token">By Reference Token</option>
|
| 276 |
+
</select>
|
| 277 |
+
</label>
|
| 278 |
+
|
| 279 |
+
<div id="opt-topk" class="sub-opt">
|
| 280 |
+
<label>K: <input type="number" id="bp-k" value="10" style="width: 50px;"></label>
|
| 281 |
+
</div>
|
| 282 |
+
|
| 283 |
+
<div id="opt-ref" class="sub-opt hidden">
|
| 284 |
+
<label>Ref Token ID: <input type="number" id="bp-ref-id" placeholder="ID"></label>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
<!-- Column 2: Layer Config -->
|
| 290 |
+
<div class="control-col">
|
| 291 |
+
<h3>Layer Connection</h3>
|
| 292 |
+
<label>Layers (Comma-sep, increasing):
|
| 293 |
+
<input type="text" id="layers-list" placeholder="e.g. 0, 10, 27" style="width: 100%;">
|
| 294 |
+
</label>
|
| 295 |
+
<div style="margin-top: 5px;">
|
| 296 |
+
<small>Presets:</small>
|
| 297 |
+
<button id="btn-layer-default" class="btn-micro">Recommended</button>
|
| 298 |
+
<button id="btn-layer-all" class="btn-micro">All Layers</button>
|
| 299 |
+
</div>
|
| 300 |
+
<button id="btn-compute-circuit" style="margin-top: 10px;">Visualize Connection</button>
|
| 301 |
+
</div>
|
| 302 |
+
|
| 303 |
+
<!-- Column 3: Vis Controls -->
|
| 304 |
+
<div class="control-col">
|
| 305 |
+
<h3>Vis Settings</h3>
|
| 306 |
+
<div class="setting-item">
|
| 307 |
+
<label><input type="checkbox" id="show-all-tokens"> Show All Tokens</label>
|
| 308 |
+
</div>
|
| 309 |
+
<div class="setting-item">
|
| 310 |
+
<label><input type="checkbox" id="hide-bos-node"> Hide First Token (BOS)</label>
|
| 311 |
+
</div>
|
| 312 |
+
<div class="setting-item">
|
| 313 |
+
<label><input type="checkbox" id="show-node-values" checked> Show Node Values</label>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<!-- New Pruning Controls -->
|
| 317 |
+
<div class="setting-item">
|
| 318 |
+
<label>Pruning Mode:</label>
|
| 319 |
+
<select id="pruning-mode" style="width: 100%;">
|
| 320 |
+
<option value="by_per_layer_cum_mass_percentile" selected>By Top-P Mass (Layer)</option>
|
| 321 |
+
<option value="by_global_threshold">By Global Threshold</option>
|
| 322 |
+
</select>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
<div id="ctrl-top-p" class="setting-item">
|
| 326 |
+
<label>Top-P Mass (0-1):</label>
|
| 327 |
+
<div class="input-row">
|
| 328 |
+
<input type="range" id="vis-top-p" min="0" max="1" step="0.01" value="0.85">
|
| 329 |
+
<input type="number" id="val-vis-top-p" min="0" max="1" step="0.01" value="0.85" style="width: 60px;">
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
|
| 333 |
+
<div id="ctrl-global-thresh" class="setting-item hidden">
|
| 334 |
+
<label>Global Threshold:</label>
|
| 335 |
+
<div class="input-row">
|
| 336 |
+
<input type="number" id="val-global-thresh" value="0.01" step="0.001" style="width: 100%;">
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<div class="setting-item">
|
| 341 |
+
<label>Line Strength:</label>
|
| 342 |
+
<div class="input-row">
|
| 343 |
+
<input type="range" id="vis-strength" min="2" max="20" step="0.1" value="6">
|
| 344 |
+
<input type="number" id="val-vis-strength" min="2" max="20" step="0.1" value="6" style="width: 60px;">
|
| 345 |
+
</div>
|
| 346 |
+
</div>
|
| 347 |
+
<div class="setting-item">
|
| 348 |
+
<label>Layer Spacing (px):</label>
|
| 349 |
+
<div class="input-row">
|
| 350 |
+
<input type="range" id="vis-layer-spacing" min="10" max="200" step="10" value="50">
|
| 351 |
+
<input type="number" id="val-vis-layer-spacing" min="10" max="200" step="10" value="50" style="width: 60px;">
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
|
| 355 |
+
<div class="setting-item" style="margin-top: 15px;">
|
| 356 |
+
<button id="btn-save-graph" style="width: 48%; background-color: #2ecc71;">Save PNG</button>
|
| 357 |
+
<button id="btn-save-pdf" style="width: 48%; background-color: #e74c3c;">Save PDF</button>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
|
| 362 |
+
<div id="canvas-container">
|
| 363 |
+
<canvas id="circuit-canvas"></canvas>
|
| 364 |
+
<div id="tooltip"></div>
|
| 365 |
+
</div>
|
| 366 |
+
|
| 367 |
+
<div id="vis-legend">
|
| 368 |
+
<span class="legend-box pos"></span> Pos Relevance
|
| 369 |
+
<span class="legend-box neg"></span> Neg Relevance
|
| 370 |
+
</div>
|
| 371 |
+
</section>
|
| 372 |
+
</main>
|
| 373 |
+
</div>
|
| 374 |
+
|
| 375 |
+
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
| 376 |
+
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
|
| 377 |
+
|
| 378 |
+
<script src="js/main.js"></script>
|
| 379 |
+
<div id="loading-overlay" class="hidden">
|
| 380 |
+
<div class="spinner"></div>
|
| 381 |
+
<div id="loading-message">Processing...</div>
|
| 382 |
+
</div>
|
| 383 |
+
</body>
|
| 384 |
+
</html>
|
frontend/js/main.js
ADDED
|
@@ -0,0 +1,2231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const API_BASE = "/api";
|
| 2 |
+
|
| 3 |
+
// Loading Spinner Helpers
|
| 4 |
+
const elLoadingOverlay = document.getElementById('loading-overlay');
|
| 5 |
+
const elLoadingMessage = document.getElementById('loading-message');
|
| 6 |
+
|
| 7 |
+
function showLoading(msg="Processing...") {
|
| 8 |
+
if(elLoadingMessage) elLoadingMessage.textContent = msg;
|
| 9 |
+
if(elLoadingOverlay) {
|
| 10 |
+
elLoadingOverlay.classList.remove('hidden');
|
| 11 |
+
// Force browser repaint
|
| 12 |
+
void elLoadingOverlay.offsetWidth;
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
function hideLoading() {
|
| 17 |
+
if(elLoadingOverlay) elLoadingOverlay.classList.add('hidden');
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// State
|
| 21 |
+
let appState = {
|
| 22 |
+
tokens: [],
|
| 23 |
+
selectedTargetToken: null,
|
| 24 |
+
selectedContrastToken: null,
|
| 25 |
+
circuitData: null,
|
| 26 |
+
mouseX: -1,
|
| 27 |
+
mouseY: -1,
|
| 28 |
+
extraTokens: [], // Stores objects: { type: 'id'|'str', value: ... }
|
| 29 |
+
lastRelevance: null // Cache for attribution map re-rendering
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// Selection State for Graph
|
| 33 |
+
let selectedNode = null;
|
| 34 |
+
let lastRenderedNodes = [];
|
| 35 |
+
|
| 36 |
+
// DOM Elements
|
| 37 |
+
const elModelSeries = document.getElementById('model-series');
|
| 38 |
+
const elModelSelect = document.getElementById('model-select');
|
| 39 |
+
const elModelRevision = document.getElementById('model-revision');
|
| 40 |
+
const elModelPath = document.getElementById('model-path');
|
| 41 |
+
const elModelDtype = document.getElementById('model-dtype');
|
| 42 |
+
const elQuant = document.getElementById('quant-4bit');
|
| 43 |
+
const elBtnLoad = document.getElementById('btn-load-model');
|
| 44 |
+
const elStatus = document.getElementById('model-status');
|
| 45 |
+
|
| 46 |
+
const elTraceDataset = document.getElementById('trace-dataset');
|
| 47 |
+
const elTraceFile = document.getElementById('trace-file');
|
| 48 |
+
const elSectionTraceExplore = document.getElementById('section-trace-explore');
|
| 49 |
+
const elContainerExploreOriginal = document.getElementById('container-explore-original');
|
| 50 |
+
const elContainerExplore4b = document.getElementById('container-explore-4b');
|
| 51 |
+
const elHeaderExploreOther = document.getElementById('header-explore-other');
|
| 52 |
+
|
| 53 |
+
const elPrompt = document.getElementById('prompt-input');
|
| 54 |
+
const elCompletion = document.getElementById('completion-input');
|
| 55 |
+
const elPromptOrig = document.getElementById('prompt-orig-completion');
|
| 56 |
+
const elAppendBos = document.getElementById('append-bos');
|
| 57 |
+
const elBtnLogits = document.getElementById('btn-compute-logits');
|
| 58 |
+
const elBtnLocateError = document.getElementById('btn-locate-error');
|
| 59 |
+
const elErrorLocateStatus = document.getElementById('error-locate-status');
|
| 60 |
+
|
| 61 |
+
const elSectionErrorResult = document.getElementById('section-error-result');
|
| 62 |
+
const elSectionContinueAnalysis = document.getElementById('section-continue-analysis');
|
| 63 |
+
const elErrorExplanation = document.getElementById('error-explanation');
|
| 64 |
+
const elTruncatedText = document.getElementById('truncated-text');
|
| 65 |
+
const elBtnUseTruncated = document.getElementById('btn-use-truncated');
|
| 66 |
+
const elBtnCancelError = document.getElementById('btn-cancel-error');
|
| 67 |
+
|
| 68 |
+
const elSectionLogits = document.getElementById('section-logits');
|
| 69 |
+
const elSectionInputAttr = document.getElementById('section-input-attr');
|
| 70 |
+
const elLogitsTableBody = document.querySelector('#logits-table tbody');
|
| 71 |
+
const elTopTokenDisplay = document.getElementById('top-token-display');
|
| 72 |
+
const elContrastTokenDisplay = document.getElementById('contrast-token-display');
|
| 73 |
+
|
| 74 |
+
const elSectionCircuit = document.getElementById('section-circuit');
|
| 75 |
+
const elBpMode = document.getElementById('bp-mode');
|
| 76 |
+
const elBpStrategy = document.getElementById('bp-strategy');
|
| 77 |
+
const elBpK = document.getElementById('bp-k');
|
| 78 |
+
const elBpRefId = document.getElementById('bp-ref-id');
|
| 79 |
+
const elOptTopk = document.getElementById('opt-topk');
|
| 80 |
+
const elOptRef = document.getElementById('opt-ref');
|
| 81 |
+
|
| 82 |
+
const elTargetLayer = document.getElementById('target-layer');
|
| 83 |
+
const elSourceLayer = document.getElementById('source-layer');
|
| 84 |
+
const elLayersList = document.getElementById('layers-list'); // NEW INPUT
|
| 85 |
+
const elBtnCircuit = document.getElementById('btn-compute-circuit');
|
| 86 |
+
|
| 87 |
+
const elVisStrength = document.getElementById('vis-strength');
|
| 88 |
+
const elValVisStrength = document.getElementById('val-vis-strength');
|
| 89 |
+
|
| 90 |
+
// New Controls
|
| 91 |
+
const elPruningMode = document.getElementById('pruning-mode');
|
| 92 |
+
const elVisTopP = document.getElementById('vis-top-p');
|
| 93 |
+
const elValVisTopP = document.getElementById('val-vis-top-p');
|
| 94 |
+
const elValGlobalThresh = document.getElementById('val-global-thresh');
|
| 95 |
+
const elCtrlTopP = document.getElementById('ctrl-top-p');
|
| 96 |
+
const elCtrlGlobalThresh = document.getElementById('ctrl-global-thresh');
|
| 97 |
+
|
| 98 |
+
const elShowAllTokens = document.getElementById('show-all-tokens');
|
| 99 |
+
const elBtnLayerDefault = document.getElementById('btn-layer-default');
|
| 100 |
+
const elBtnLayerAll = document.getElementById('btn-layer-all');
|
| 101 |
+
|
| 102 |
+
// Input Attribution Elements
|
| 103 |
+
const elAttrBpMode = document.getElementById('attr-bp-mode');
|
| 104 |
+
const elAttrBpStrategy = document.getElementById('attr-bp-strategy');
|
| 105 |
+
const elAttrBpRefId = document.getElementById('attr-bp-ref-id');
|
| 106 |
+
const elAttrOptRef = document.getElementById('attr-opt-ref');
|
| 107 |
+
const elAttrHideBos = document.getElementById('attr-hide-bos');
|
| 108 |
+
const elAttrHideSpecial = document.getElementById('attr-hide-special');
|
| 109 |
+
const elAttrReverseSign = document.getElementById('attr-reverse-sign');
|
| 110 |
+
const elBtnComputeAttr = document.getElementById('btn-compute-attr');
|
| 111 |
+
const elBtnSaveGraph = document.getElementById('btn-save-graph'); // Save Button
|
| 112 |
+
const elInputAttributionDisplay = document.getElementById('input-attribution-display');
|
| 113 |
+
const elAttrDiffStrategyOpts = document.getElementById('attr-diff-strategy-opts'); // Container for strategy ops
|
| 114 |
+
|
| 115 |
+
const elTokenCountDisplay = document.getElementById('token-count-display');
|
| 116 |
+
const elVisLayerSpacing = document.getElementById('vis-layer-spacing');
|
| 117 |
+
const elValVisLayerSpacing = document.getElementById('val-vis-layer-spacing');
|
| 118 |
+
|
| 119 |
+
const elCanvas = document.getElementById('circuit-canvas');
|
| 120 |
+
const elTooltip = document.getElementById('tooltip');
|
| 121 |
+
|
| 122 |
+
// Event Listeners
|
| 123 |
+
console.log("Attaching event listeners...");
|
| 124 |
+
try {
|
| 125 |
+
if(elBtnLoad) elBtnLoad.addEventListener('click', loadModel);
|
| 126 |
+
else console.error("elBtnLoad not found");
|
| 127 |
+
|
| 128 |
+
if(elBtnLocateError) elBtnLocateError.addEventListener('click', locateErrorToken);
|
| 129 |
+
else console.warn("elBtnLocateError not found");
|
| 130 |
+
|
| 131 |
+
if(elBtnUseTruncated) elBtnUseTruncated.addEventListener('click', useTruncatedText);
|
| 132 |
+
else console.warn("elBtnUseTruncated not found");
|
| 133 |
+
|
| 134 |
+
if(elBtnCancelError) elBtnCancelError.addEventListener('click', cancelErrorLocation);
|
| 135 |
+
else console.warn("elBtnCancelError not found");
|
| 136 |
+
|
| 137 |
+
if(elBtnLogits) elBtnLogits.addEventListener('click', () => computeLogits(false));
|
| 138 |
+
|
| 139 |
+
// Add Token Feature
|
| 140 |
+
const elBtnAddToken = document.getElementById('btn-add-token');
|
| 141 |
+
const elAddTokenInput = document.getElementById('add-token-input');
|
| 142 |
+
|
| 143 |
+
if(elBtnAddToken && elAddTokenInput) {
|
| 144 |
+
elBtnAddToken.addEventListener('click', () => {
|
| 145 |
+
const val = elAddTokenInput.value.trim();
|
| 146 |
+
if(!val) return;
|
| 147 |
+
|
| 148 |
+
// Parse: is it ID or String?
|
| 149 |
+
// If numeric, treat as ID.
|
| 150 |
+
if (/^\d+$/.test(val)) {
|
| 151 |
+
const id = parseInt(val);
|
| 152 |
+
appState.extraTokens.push({ type: 'id', value: id });
|
| 153 |
+
} else {
|
| 154 |
+
appState.extraTokens.push({ type: 'str', value: val });
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
elAddTokenInput.value = ""; // Clear input
|
| 158 |
+
computeLogits(true); // Keep extras!
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
// Allow Enter key
|
| 162 |
+
elAddTokenInput.addEventListener('keypress', (e) => {
|
| 163 |
+
if (e.key === 'Enter') elBtnAddToken.click();
|
| 164 |
+
});
|
| 165 |
+
}
|
| 166 |
+
if(elBpMode) elBpMode.addEventListener('change', updateBpOptions);
|
| 167 |
+
if(elBpStrategy) elBpStrategy.addEventListener('change', updateBpOptions);
|
| 168 |
+
if(elBtnCircuit) elBtnCircuit.addEventListener('click', computeCircuit);
|
| 169 |
+
if(elShowAllTokens) elShowAllTokens.addEventListener('change', drawCircuit);
|
| 170 |
+
|
| 171 |
+
if(elBtnLayerDefault) elBtnLayerDefault.addEventListener('click', () => setLayerPreset('default'));
|
| 172 |
+
if(elBtnLayerAll) elBtnLayerAll.addEventListener('click', () => setLayerPreset('all'));
|
| 173 |
+
|
| 174 |
+
// Input Attribution Listeners
|
| 175 |
+
if(elAttrBpMode) elAttrBpMode.addEventListener('change', updateAttrBpOptions);
|
| 176 |
+
if(elAttrBpStrategy) elAttrBpStrategy.addEventListener('change', updateAttrBpOptions);
|
| 177 |
+
// Re-render when checkboxes change (if data exists)
|
| 178 |
+
const reRenderAttr = () => { if(appState.lastRelevance) renderAttributionMap(appState.lastRelevance, appState.tokens); };
|
| 179 |
+
if(elAttrHideBos) elAttrHideBos.addEventListener('change', reRenderAttr);
|
| 180 |
+
if(elAttrHideSpecial) elAttrHideSpecial.addEventListener('change', reRenderAttr);
|
| 181 |
+
if(elAttrReverseSign) elAttrReverseSign.addEventListener('change', reRenderAttr);
|
| 182 |
+
|
| 183 |
+
if(elBtnComputeAttr) elBtnComputeAttr.addEventListener('click', computeInputAttribution);
|
| 184 |
+
if(elBtnSaveGraph) elBtnSaveGraph.addEventListener('click', saveCircuitGraph);
|
| 185 |
+
const elBtnSavePdf = document.getElementById('btn-save-pdf');
|
| 186 |
+
if(elBtnSavePdf) elBtnSavePdf.addEventListener('click', saveCircuitGraphPDF);
|
| 187 |
+
|
| 188 |
+
// Attrib Save
|
| 189 |
+
const elBtnSaveAttrPng = document.getElementById('btn-save-attr-png');
|
| 190 |
+
const elBtnSaveAttrPdf = document.getElementById('btn-save-attr-pdf');
|
| 191 |
+
if(elBtnSaveAttrPng) elBtnSaveAttrPng.addEventListener('click', saveAttributionMapPNG);
|
| 192 |
+
if(elBtnSaveAttrPdf) elBtnSaveAttrPdf.addEventListener('click', saveAttributionMapPDF);
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
if(elPruningMode) elPruningMode.addEventListener('change', updatePruningControls);
|
| 196 |
+
|
| 197 |
+
console.log("Event listeners attached.");
|
| 198 |
+
} catch(e) {
|
| 199 |
+
console.error("Error attaching listeners:", e);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Visualization Controls (Slider <-> Input Sync)
|
| 203 |
+
function bindControl(slider, input, callback, debounceMs = 0) {
|
| 204 |
+
if (!slider || !input) {
|
| 205 |
+
// console.error("bindControl missing elements", slider, input);
|
| 206 |
+
return;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
let debounceTimer = null;
|
| 210 |
+
const trigger = () => {
|
| 211 |
+
if (debounceMs > 0) {
|
| 212 |
+
if (debounceTimer) clearTimeout(debounceTimer);
|
| 213 |
+
debounceTimer = setTimeout(() => {
|
| 214 |
+
if(callback) callback();
|
| 215 |
+
}, debounceMs);
|
| 216 |
+
} else {
|
| 217 |
+
if(callback) callback();
|
| 218 |
+
}
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
// Slider updates Input
|
| 222 |
+
slider.addEventListener('input', () => {
|
| 223 |
+
input.value = slider.value;
|
| 224 |
+
trigger();
|
| 225 |
+
});
|
| 226 |
+
// Input updates Slider
|
| 227 |
+
input.addEventListener('input', () => {
|
| 228 |
+
let val = parseFloat(input.value);
|
| 229 |
+
if (!isNaN(val)) {
|
| 230 |
+
// Clamp to slider limits
|
| 231 |
+
val = Math.max(parseFloat(slider.min), Math.min(parseFloat(slider.max), val));
|
| 232 |
+
slider.value = val;
|
| 233 |
+
trigger();
|
| 234 |
+
}
|
| 235 |
+
});
|
| 236 |
+
}
|
| 237 |
+
console.log("Binding controls...");
|
| 238 |
+
// Bind with specific update functions for efficiency
|
| 239 |
+
// Layout updates (heavy): Edge/Node Thresholds, Spacing - Debounced
|
| 240 |
+
// bindControl(elVisThreshold, elValVisThreshold, updateCircuitLayout, 300);
|
| 241 |
+
// bindControl(elVisNodeThreshold, elValVisNodeThreshold, updateCircuitLayout, 300);
|
| 242 |
+
|
| 243 |
+
// NEW Bindings for TopP (Callback: computeCircuit, Debounced)
|
| 244 |
+
bindControl(elVisTopP, elValVisTopP, computeCircuit, 600);
|
| 245 |
+
|
| 246 |
+
// Layout Geometry updates (moved to light): Spacing
|
| 247 |
+
bindControl(elVisLayerSpacing, elValVisLayerSpacing, drawCircuit, 0);
|
| 248 |
+
// Visual updates (light): Strength
|
| 249 |
+
bindControl(elVisStrength, elValVisStrength, drawCircuit, 0);
|
| 250 |
+
|
| 251 |
+
function updatePruningControls() {
|
| 252 |
+
const mode = elPruningMode.value;
|
| 253 |
+
if (mode === 'by_per_layer_cum_mass_percentile') {
|
| 254 |
+
elCtrlTopP.classList.remove('hidden');
|
| 255 |
+
elCtrlGlobalThresh.classList.add('hidden');
|
| 256 |
+
} else {
|
| 257 |
+
elCtrlTopP.classList.add('hidden');
|
| 258 |
+
elCtrlGlobalThresh.classList.remove('hidden');
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
// Init State
|
| 262 |
+
updatePruningControls();
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
// Functions
|
| 267 |
+
|
| 268 |
+
async function loadModel() {
|
| 269 |
+
console.log("loadModel called");
|
| 270 |
+
updateStatus('Loading...', 'normal');
|
| 271 |
+
showLoading("Loading Model...");
|
| 272 |
+
elBtnLoad.disabled = true;
|
| 273 |
+
|
| 274 |
+
try {
|
| 275 |
+
const response = await fetch(`${API_BASE}/load_model`, {
|
| 276 |
+
method: 'POST',
|
| 277 |
+
headers: {'Content-Type': 'application/json'},
|
| 278 |
+
body: JSON.stringify({
|
| 279 |
+
model_path: elModelPath.value,
|
| 280 |
+
quantization_4bit: elQuant.checked,
|
| 281 |
+
dtype: elModelDtype.value,
|
| 282 |
+
revision: elModelRevision ? elModelRevision.value : null,
|
| 283 |
+
lrp_rule: document.getElementById('model-lrp-rule').value
|
| 284 |
+
})
|
| 285 |
+
});
|
| 286 |
+
|
| 287 |
+
const data = await response.json();
|
| 288 |
+
if (response.ok) {
|
| 289 |
+
updateStatus('Loaded', 'success');
|
| 290 |
+
if(elBtnLogits) elBtnLogits.disabled = false;
|
| 291 |
+
if(elBtnLocateError) elBtnLocateError.disabled = false;
|
| 292 |
+
if(data.n_layers) {
|
| 293 |
+
appState.n_layers = data.n_layers;
|
| 294 |
+
// Update default presets
|
| 295 |
+
generateLayerPresets();
|
| 296 |
+
}
|
| 297 |
+
} else {
|
| 298 |
+
throw new Error(data.detail || 'Failed to load model');
|
| 299 |
+
}
|
| 300 |
+
} catch (e) {
|
| 301 |
+
updateStatus(`Error: ${e.message}`, 'error');
|
| 302 |
+
alert(e.message);
|
| 303 |
+
} finally {
|
| 304 |
+
elBtnLoad.disabled = false;
|
| 305 |
+
hideLoading();
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
async function locateErrorToken() {
|
| 310 |
+
const prompt = elPrompt.value.trim();
|
| 311 |
+
const completion = elCompletion.value.trim();
|
| 312 |
+
|
| 313 |
+
if (!prompt) {
|
| 314 |
+
alert("Please enter a prompt.");
|
| 315 |
+
return;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
if (!completion) {
|
| 319 |
+
alert("Please enter a completion.");
|
| 320 |
+
return;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
elBtnLocateError.disabled = true;
|
| 324 |
+
elErrorLocateStatus.textContent = "Locating error token...";
|
| 325 |
+
showLoading("Analyzing error token...");
|
| 326 |
+
|
| 327 |
+
try {
|
| 328 |
+
const res = await fetch(`${API_BASE}/locate_err_token`, {
|
| 329 |
+
method: 'POST',
|
| 330 |
+
headers: { 'Content-Type': 'application/json' },
|
| 331 |
+
body: JSON.stringify({
|
| 332 |
+
prompt: prompt,
|
| 333 |
+
completion: completion
|
| 334 |
+
})
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
+
const data = await res.json();
|
| 338 |
+
|
| 339 |
+
if (res.ok && data.status === "success") {
|
| 340 |
+
elErrorLocateStatus.textContent = "Error token located successfully.";
|
| 341 |
+
|
| 342 |
+
// Display results
|
| 343 |
+
elErrorExplanation.textContent = data.explanation || "No explanation provided.";
|
| 344 |
+
elTruncatedText.value = data.truncated_text || "";
|
| 345 |
+
|
| 346 |
+
// Show result section
|
| 347 |
+
elSectionErrorResult.classList.remove('hidden');
|
| 348 |
+
|
| 349 |
+
} else {
|
| 350 |
+
elErrorLocateStatus.textContent = "Failed to locate error token.";
|
| 351 |
+
alert(`Error: ${data.message || data.detail || 'Unknown error'}`);
|
| 352 |
+
}
|
| 353 |
+
} catch (e) {
|
| 354 |
+
elErrorLocateStatus.textContent = "Request failed.";
|
| 355 |
+
alert(`Network error: ${e.message}`);
|
| 356 |
+
} finally {
|
| 357 |
+
hideLoading();
|
| 358 |
+
elBtnLocateError.disabled = false;
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
function useTruncatedText() {
|
| 363 |
+
const truncatedText = elTruncatedText.value.trim();
|
| 364 |
+
|
| 365 |
+
if (!truncatedText) {
|
| 366 |
+
alert("Truncated text is empty.");
|
| 367 |
+
return;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
// Update the prompt field with truncated text
|
| 371 |
+
elPrompt.value = truncatedText;
|
| 372 |
+
elPromptOrig.value = truncatedText;
|
| 373 |
+
|
| 374 |
+
// Hide error result section and show continue analysis section
|
| 375 |
+
elSectionErrorResult.classList.add('hidden');
|
| 376 |
+
elSectionContinueAnalysis.classList.remove('hidden');
|
| 377 |
+
|
| 378 |
+
// Clear error status
|
| 379 |
+
elErrorLocateStatus.textContent = "";
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
function cancelErrorLocation() {
|
| 383 |
+
// Hide error result section
|
| 384 |
+
elSectionErrorResult.classList.add('hidden');
|
| 385 |
+
|
| 386 |
+
// Clear error status
|
| 387 |
+
elErrorLocateStatus.textContent = "";
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
async function computeLogits(keepExtras = false) {
|
| 391 |
+
elBtnLogits.disabled = true;
|
| 392 |
+
showLoading("Computing Logits...");
|
| 393 |
+
|
| 394 |
+
// If this is a fresh run (not adding a token), clear extras
|
| 395 |
+
if (!keepExtras) {
|
| 396 |
+
appState.extraTokens = [];
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
elSectionLogits.classList.add('hidden');
|
| 400 |
+
if(elSectionInputAttr) elSectionInputAttr.classList.add('hidden');
|
| 401 |
+
elSectionCircuit.classList.add('hidden');
|
| 402 |
+
|
| 403 |
+
// Prepare extras
|
| 404 |
+
const extraIds = [];
|
| 405 |
+
const extraStrs = [];
|
| 406 |
+
|
| 407 |
+
appState.extraTokens.forEach(item => {
|
| 408 |
+
if(item.type === 'id') extraIds.push(item.value);
|
| 409 |
+
else extraStrs.push(item.value);
|
| 410 |
+
});
|
| 411 |
+
|
| 412 |
+
try {
|
| 413 |
+
const response = await fetch(`${API_BASE}/compute_logits`, {
|
| 414 |
+
method: 'POST',
|
| 415 |
+
headers: {'Content-Type': 'application/json'},
|
| 416 |
+
body: JSON.stringify({
|
| 417 |
+
prompt: elPrompt.value,
|
| 418 |
+
is_append_bos: elAppendBos.checked,
|
| 419 |
+
topk: 50,
|
| 420 |
+
extra_token_ids: extraIds,
|
| 421 |
+
extra_token_strs: extraStrs,
|
| 422 |
+
capture_mid: document.getElementById('capture-mid') ? document.getElementById('capture-mid').checked : false
|
| 423 |
+
})
|
| 424 |
+
});
|
| 425 |
+
|
| 426 |
+
const res = await response.json();
|
| 427 |
+
if (response.ok) {
|
| 428 |
+
renderLogitsTable(res.data);
|
| 429 |
+
appState.tokens = res.tokens;
|
| 430 |
+
|
| 431 |
+
// Display token count
|
| 432 |
+
if (elTokenCountDisplay) {
|
| 433 |
+
elTokenCountDisplay.textContent = `Total Input Tokens: ${res.tokens.length}`;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
elSectionLogits.classList.remove('hidden');
|
| 437 |
+
if(elSectionInputAttr) elSectionInputAttr.classList.remove('hidden');
|
| 438 |
+
elSectionCircuit.classList.remove('hidden');
|
| 439 |
+
|
| 440 |
+
// Set default selections
|
| 441 |
+
if (res.data.length > 0) {
|
| 442 |
+
appState.selectedTargetToken = res.data[0];
|
| 443 |
+
elTopTokenDisplay.textContent = `${appState.selectedTargetToken.token_str} (ID: ${appState.selectedTargetToken.token_id})`;
|
| 444 |
+
}
|
| 445 |
+
} else {
|
| 446 |
+
alert(`Error: ${res.detail}`);
|
| 447 |
+
}
|
| 448 |
+
} catch (e) {
|
| 449 |
+
alert(`Error: ${e.message}`);
|
| 450 |
+
} finally {
|
| 451 |
+
elBtnLogits.disabled = false;
|
| 452 |
+
hideLoading();
|
| 453 |
+
}
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
function renderLogitsTable(data) {
|
| 457 |
+
elLogitsTableBody.innerHTML = '';
|
| 458 |
+
data.forEach(item => {
|
| 459 |
+
// Robust escaping for the onclick handler string
|
| 460 |
+
const escapedTokenStr = formatTokenForDisplay(item.token_str, 'data');
|
| 461 |
+
|
| 462 |
+
// VISUAL ESCAPE for the table cell
|
| 463 |
+
let visualTokenStr = formatTokenForDisplay(item.token_str, 'visual');
|
| 464 |
+
|
| 465 |
+
const row = document.createElement('tr');
|
| 466 |
+
row.innerHTML = `
|
| 467 |
+
<td>${item.rank}</td>
|
| 468 |
+
<td>${item.token_id}</td>
|
| 469 |
+
<td>${visualTokenStr}</td>
|
| 470 |
+
<td>${item.logit.toFixed(4)}</td>
|
| 471 |
+
<td><button id="btn-select-${item.token_id}" class="btn-select-ref" onclick="selectContrast(${item.token_id}, '${escapedTokenStr}', ${item.logit})">Select for Contrast</button></td>
|
| 472 |
+
<td>
|
| 473 |
+
<button class="btn-generate-cont" onclick="generateContinuation(${item.token_id}, this)">Generate</button>
|
| 474 |
+
<div class="gen-result" style="font-size: 0.85em; color: #555; margin-top: 4px; max-width: 300px; white-space: pre-wrap;"></div>
|
| 475 |
+
</td>
|
| 476 |
+
`;
|
| 477 |
+
elLogitsTableBody.appendChild(row);
|
| 478 |
+
});
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
window.generateContinuation = async function(tokenId, btn) {
|
| 482 |
+
const originalText = btn.textContent;
|
| 483 |
+
btn.disabled = true;
|
| 484 |
+
const elGenMax = document.getElementById('gen-max-tokens');
|
| 485 |
+
const maxNewTokens = elGenMax ? parseInt(elGenMax.value) : 30;
|
| 486 |
+
|
| 487 |
+
btn.textContent = `Generating (${maxNewTokens})...`;
|
| 488 |
+
const resultDiv = btn.nextElementSibling;
|
| 489 |
+
resultDiv.textContent = "";
|
| 490 |
+
|
| 491 |
+
try {
|
| 492 |
+
const payload = {
|
| 493 |
+
prompt: elPrompt.value,
|
| 494 |
+
max_new_tokens: maxNewTokens,
|
| 495 |
+
append_token_id: tokenId
|
| 496 |
+
};
|
| 497 |
+
|
| 498 |
+
const response = await fetch(`${API_BASE}/generate`, {
|
| 499 |
+
method: 'POST',
|
| 500 |
+
headers: {'Content-Type': 'application/json'},
|
| 501 |
+
body: JSON.stringify(payload)
|
| 502 |
+
});
|
| 503 |
+
|
| 504 |
+
if (!response.ok) {
|
| 505 |
+
const err = await response.json();
|
| 506 |
+
throw new Error(err.detail || "Generation failed");
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
const data = await response.json();
|
| 510 |
+
resultDiv.textContent = data.generated_text;
|
| 511 |
+
|
| 512 |
+
} catch(e) {
|
| 513 |
+
resultDiv.textContent = "Error: " + e.message;
|
| 514 |
+
resultDiv.style.color = "red";
|
| 515 |
+
} finally {
|
| 516 |
+
btn.disabled = false;
|
| 517 |
+
btn.textContent = originalText;
|
| 518 |
+
}
|
| 519 |
+
};
|
| 520 |
+
|
| 521 |
+
window.selectContrast = function(id, name, logit) {
|
| 522 |
+
appState.selectedContrastToken = { id, name, logit };
|
| 523 |
+
|
| 524 |
+
// Calculate Diff
|
| 525 |
+
let diffText = "";
|
| 526 |
+
if (appState.selectedTargetToken) {
|
| 527 |
+
// Find top logit (usually the first one in the list if sorted, or stored in appState)
|
| 528 |
+
// appState.selectedTargetToken comes from computeLogits result[0]
|
| 529 |
+
const topLogit = appState.selectedTargetToken.logit;
|
| 530 |
+
const diff = topLogit - logit;
|
| 531 |
+
diffText = ` | Logit Diff: ${diff.toFixed(4)}`;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
elContrastTokenDisplay.textContent = `${name} (ID: ${id})${diffText}`;
|
| 535 |
+
|
| 536 |
+
// Visual Updates
|
| 537 |
+
document.querySelectorAll('.btn-select-ref').forEach(btn => {
|
| 538 |
+
btn.textContent = "Select for Contrast";
|
| 539 |
+
btn.classList.remove('selected');
|
| 540 |
+
btn.style.backgroundColor = ""; // Reset inline style if any (or rely on class)
|
| 541 |
+
});
|
| 542 |
+
|
| 543 |
+
const activeBtn = document.getElementById(`btn-select-${id}`);
|
| 544 |
+
if (activeBtn) {
|
| 545 |
+
activeBtn.textContent = "Selected for Contrast";
|
| 546 |
+
activeBtn.classList.add('selected');
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
// Auto Update UI settings
|
| 550 |
+
elBpMode.value = "logit_diff";
|
| 551 |
+
updateBpOptions();
|
| 552 |
+
elBpStrategy.value = "by_ref_token";
|
| 553 |
+
updateBpOptions();
|
| 554 |
+
elBpRefId.value = id;
|
| 555 |
+
|
| 556 |
+
// Auto Update Input Attribution settings
|
| 557 |
+
if (elAttrBpMode) {
|
| 558 |
+
elAttrBpMode.value = "logit_diff";
|
| 559 |
+
updateAttrBpOptions();
|
| 560 |
+
}
|
| 561 |
+
if (elAttrBpStrategy) {
|
| 562 |
+
elAttrBpStrategy.value = "by_ref_token";
|
| 563 |
+
updateAttrBpOptions();
|
| 564 |
+
}
|
| 565 |
+
if (elAttrBpRefId) {
|
| 566 |
+
elAttrBpRefId.value = id;
|
| 567 |
+
}
|
| 568 |
+
};
|
| 569 |
+
|
| 570 |
+
function updateBpOptions() {
|
| 571 |
+
const isDiff = elBpMode.value === "logit_diff";
|
| 572 |
+
document.getElementById('diff-strategy-opts').style.display = isDiff ? 'block' : 'none';
|
| 573 |
+
|
| 574 |
+
if (isDiff) {
|
| 575 |
+
const strategy = elBpStrategy.value;
|
| 576 |
+
elOptTopk.classList.toggle('hidden', strategy !== 'by_topk_avg');
|
| 577 |
+
elOptRef.classList.toggle('hidden', strategy !== 'by_ref_token');
|
| 578 |
+
}
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
function updateAttrBpOptions() {
|
| 582 |
+
const isDiff = elAttrBpMode.value === "logit_diff";
|
| 583 |
+
if (elAttrDiffStrategyOpts) {
|
| 584 |
+
elAttrDiffStrategyOpts.style.display = isDiff ? 'block' : 'none';
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
if (isDiff) {
|
| 588 |
+
const strategy = elAttrBpStrategy.value;
|
| 589 |
+
if (elAttrOptRef) {
|
| 590 |
+
elAttrOptRef.classList.toggle('hidden', strategy !== 'by_ref_token');
|
| 591 |
+
}
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
async function computeInputAttribution() {
|
| 596 |
+
if (!appState.selectedTargetToken) {
|
| 597 |
+
alert("Please calculate logits and select a target token first.");
|
| 598 |
+
return;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
// Default values if inputs are missing (e.g. reused from circuit config or hardcoded)
|
| 602 |
+
const bpK = 10;
|
| 603 |
+
|
| 604 |
+
const payload = {
|
| 605 |
+
target_token_id: appState.selectedTargetToken.token_id,
|
| 606 |
+
contrast_token_id: appState.selectedContrastToken ? appState.selectedContrastToken.id : null,
|
| 607 |
+
backprop_config: {
|
| 608 |
+
mode: elAttrBpMode.value,
|
| 609 |
+
strategy: elAttrBpStrategy.value,
|
| 610 |
+
k: bpK,
|
| 611 |
+
ref_token_id: elAttrBpRefId && elAttrBpRefId.value ? parseInt(elAttrBpRefId.value) : null
|
| 612 |
+
}
|
| 613 |
+
};
|
| 614 |
+
|
| 615 |
+
elInputAttributionDisplay.innerHTML = "Computing...";
|
| 616 |
+
elBtnComputeAttr.disabled = true;
|
| 617 |
+
|
| 618 |
+
try {
|
| 619 |
+
const resp = await fetch(`${API_BASE}/compute_input_attribution`, {
|
| 620 |
+
method: 'POST',
|
| 621 |
+
headers: {'Content-Type': 'application/json'},
|
| 622 |
+
body: JSON.stringify(payload)
|
| 623 |
+
});
|
| 624 |
+
|
| 625 |
+
if (!resp.ok) {
|
| 626 |
+
const err = await resp.json();
|
| 627 |
+
const errMsg = (typeof err.detail === 'object') ? JSON.stringify(err.detail) : (err.detail || "Request failed");
|
| 628 |
+
throw new Error(errMsg);
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
const data = await resp.json();
|
| 632 |
+
// data.relevance is list of floats
|
| 633 |
+
appState.lastRelevance = data.relevance;
|
| 634 |
+
renderAttributionMap(data.relevance, appState.tokens);
|
| 635 |
+
|
| 636 |
+
} catch (e) {
|
| 637 |
+
console.error(e);
|
| 638 |
+
elInputAttributionDisplay.textContent = "Error: " + e.message;
|
| 639 |
+
} finally {
|
| 640 |
+
elBtnComputeAttr.disabled = false;
|
| 641 |
+
}
|
| 642 |
+
}
|
| 643 |
+
if (elAttrHideBos) {
|
| 644 |
+
elAttrHideBos.addEventListener('change', () => {
|
| 645 |
+
if (appState.lastRelevance && appState.tokens) {
|
| 646 |
+
renderAttributionMap(appState.lastRelevance, appState.tokens);
|
| 647 |
+
}
|
| 648 |
+
});
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
const SPECIAL_TOKENS_SET = new Set(['<|im_start|>', '<|im_end|>', '<think>', '</think>']);
|
| 653 |
+
|
| 654 |
+
function renderAttributionMap(relevance, tokens) {
|
| 655 |
+
elInputAttributionDisplay.innerHTML = "";
|
| 656 |
+
|
| 657 |
+
if (!relevance || relevance.length === 0) {
|
| 658 |
+
elInputAttributionDisplay.textContent = "No data returned.";
|
| 659 |
+
return;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
const hideBos = elAttrHideBos ? elAttrHideBos.checked : false;
|
| 663 |
+
const hideSpecial = elAttrHideSpecial ? elAttrHideSpecial.checked : false;
|
| 664 |
+
const reverseSign = elAttrReverseSign ? elAttrReverseSign.checked : false;
|
| 665 |
+
|
| 666 |
+
// Helper: is this token skipped?
|
| 667 |
+
// NOTE: This logic determines both Normalization AND Rendering visibility
|
| 668 |
+
function isSkipped(idx, tokenData) {
|
| 669 |
+
if (hideBos && idx === 0) return true;
|
| 670 |
+
|
| 671 |
+
if (hideSpecial) {
|
| 672 |
+
// tokenData could be string or object
|
| 673 |
+
let s = (typeof tokenData === 'string') ? tokenData : (tokenData.token_str || tokenData.text);
|
| 674 |
+
if (s) s = s.trim();
|
| 675 |
+
if (SPECIAL_TOKENS_SET.has(s)) return true;
|
| 676 |
+
}
|
| 677 |
+
return false;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
// Determine range for normalization (exclude BOS if hideBos is true)
|
| 681 |
+
// But we render everything from index 0 now, just styling BOS differently.
|
| 682 |
+
|
| 683 |
+
let maxAbs = 0;
|
| 684 |
+
// Calculation Loop
|
| 685 |
+
for (let i = 0; i < relevance.length; i++) {
|
| 686 |
+
if (isSkipped(i, tokens[i])) continue; // Skip excluded tokens for dynamic range calculation
|
| 687 |
+
const r = relevance[i];
|
| 688 |
+
if (Math.abs(r) > maxAbs) maxAbs = Math.abs(r);
|
| 689 |
+
}
|
| 690 |
+
if (maxAbs === 0) maxAbs = 1;
|
| 691 |
+
|
| 692 |
+
// Helper for color
|
| 693 |
+
function getColor(val) {
|
| 694 |
+
if (Math.abs(val) === 0) return 'transparent'; // optimization
|
| 695 |
+
|
| 696 |
+
// Red for positive, Blue for negative
|
| 697 |
+
const norm = val / maxAbs;
|
| 698 |
+
|
| 699 |
+
// Use HSL for better control? Or RGBA.
|
| 700 |
+
// We want a clear white text if background is dark?
|
| 701 |
+
// Or keep background light.
|
| 702 |
+
// Let's use slight alpha backgrounds.
|
| 703 |
+
|
| 704 |
+
const alpha = Math.abs(norm);
|
| 705 |
+
// Cap alpha to avoid being too dark/unreadable if we don't change text color
|
| 706 |
+
// But we want it visible.
|
| 707 |
+
const cappedAlpha = Math.min(alpha, 0.6);
|
| 708 |
+
|
| 709 |
+
// Blend with white background to avoid html2canvas alpha issues (red background bug)
|
| 710 |
+
// Foreground: Red (255, 0, 0) or Blue (0, 0, 255)
|
| 711 |
+
// Background: White (255, 255, 255)
|
| 712 |
+
// Result = FG * alpha + BG * (1 - alpha)
|
| 713 |
+
|
| 714 |
+
if (norm > 0) {
|
| 715 |
+
// Red case: R=255, G=255(1-a), B=255(1-a)
|
| 716 |
+
const gb = Math.round(255 * (1 - cappedAlpha));
|
| 717 |
+
return `rgb(255, ${gb}, ${gb})`;
|
| 718 |
+
} else {
|
| 719 |
+
// Blue case: R=255(1-a), G=255(1-a), B=255
|
| 720 |
+
const rg = Math.round(255 * (1 - cappedAlpha));
|
| 721 |
+
return `rgb(${rg}, ${rg}, 255)`;
|
| 722 |
+
}
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
tokens.forEach((tok, idx) => {
|
| 726 |
+
// Handle mismatch length if any (shouldn't happen)
|
| 727 |
+
if (idx >= relevance.length) return;
|
| 728 |
+
|
| 729 |
+
let val = relevance[idx];
|
| 730 |
+
if (reverseSign) val = -val;
|
| 731 |
+
|
| 732 |
+
const span = document.createElement("span");
|
| 733 |
+
span.className = "token-span";
|
| 734 |
+
|
| 735 |
+
// Check if this is a "Hidden" token
|
| 736 |
+
const isHidden = isSkipped(idx, tok);
|
| 737 |
+
|
| 738 |
+
if (isHidden) {
|
| 739 |
+
// Apply special style
|
| 740 |
+
span.style.color = "#aaa";
|
| 741 |
+
span.style.backgroundColor = "#f0f0f0";
|
| 742 |
+
// span.style.textDecoration = "line-through"; // Optional?
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
// tok is a string from backend
|
| 746 |
+
let tokenText = (typeof tok === 'string') ? tok : (tok.token_str || tok.text);
|
| 747 |
+
|
| 748 |
+
// Use Central Formatter
|
| 749 |
+
const visualText = formatTokenForDisplay(tokenText, 'visual');
|
| 750 |
+
|
| 751 |
+
// Special structural handling for newlines
|
| 752 |
+
const isNewline = (tokenText === '\n' || tokenText === '\r\n');
|
| 753 |
+
const isDoubleNewline = (tokenText === '\n\n' || tokenText === '\r\n\r\n');
|
| 754 |
+
|
| 755 |
+
if (isNewline || isDoubleNewline) {
|
| 756 |
+
span.textContent = visualText;
|
| 757 |
+
// Ensure it has shape/padding
|
| 758 |
+
span.style.display = "inline-block";
|
| 759 |
+
span.style.minWidth = isDoubleNewline ? "24px" : "12px";
|
| 760 |
+
span.style.textAlign = "center";
|
| 761 |
+
} else {
|
| 762 |
+
span.textContent = tokenText; // Keep original for normal text? OR use visual?
|
| 763 |
+
// Usually normal text is fine, but if it has internal newlines, visualText handles it.
|
| 764 |
+
// Let's use visualText if it differs significantly? No, visualText escapes \n.
|
| 765 |
+
// For attribution map, we want visible \n, but regular text should probably wrap?
|
| 766 |
+
// Actually, for wrapped text block, literal \n is confusing.
|
| 767 |
+
// But valid tokens usually don't have internal newlines except specifically newline tokens.
|
| 768 |
+
// Let's stick to original tokenText for normal tokens, just in case.
|
| 769 |
+
// But if visualText detected a change (like mixed \n), we might want to show it.
|
| 770 |
+
if (visualText !== tokenText) span.textContent = visualText;
|
| 771 |
+
else span.textContent = tokenText;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
const normVal = val / maxAbs;
|
| 775 |
+
if (!isHidden) {
|
| 776 |
+
span.style.backgroundColor = getColor(val);
|
| 777 |
+
}
|
| 778 |
+
span.title = `Token: "${visualText}"\nPos: ${idx}\nRaw Rel: ${val.toFixed(5)}\nNorm Rel: ${normVal.toFixed(3)}`;
|
| 779 |
+
|
| 780 |
+
elInputAttributionDisplay.appendChild(span);
|
| 781 |
+
|
| 782 |
+
// Preserve line break behavior for newlines -- DISABLED per user request
|
| 783 |
+
/*
|
| 784 |
+
if (isNewline) {
|
| 785 |
+
elInputAttributionDisplay.appendChild(document.createElement("br"));
|
| 786 |
+
} else if (isDoubleNewline) {
|
| 787 |
+
elInputAttributionDisplay.appendChild(document.createElement("br"));
|
| 788 |
+
elInputAttributionDisplay.appendChild(document.createElement("br"));
|
| 789 |
+
}
|
| 790 |
+
*/
|
| 791 |
+
});
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
async function computeCircuit() {
|
| 795 |
+
// Parse Layers List
|
| 796 |
+
// Expect: "0, 5, 20" -> [0, 5, 20]
|
| 797 |
+
const rawVal = elLayersList.value;
|
| 798 |
+
const layers = rawVal.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n));
|
| 799 |
+
|
| 800 |
+
// Sort and Validate
|
| 801 |
+
layers.sort((a,b) => a - b);
|
| 802 |
+
|
| 803 |
+
if (layers.length < 2) {
|
| 804 |
+
alert("Please provide at least 2 layers (e.g., '0, 5, 20')");
|
| 805 |
+
return;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
elLayersList.value = layers.join(', ');
|
| 809 |
+
|
| 810 |
+
elBtnCircuit.disabled = true;
|
| 811 |
+
elBtnCircuit.textContent = "Computing...";
|
| 812 |
+
|
| 813 |
+
// Progress UI
|
| 814 |
+
const elProgressContainer = document.getElementById("compute-progress-container");
|
| 815 |
+
const elProgressBar = document.getElementById("compute-progress-bar");
|
| 816 |
+
const elProgressStatus = document.getElementById("progress-status-text");
|
| 817 |
+
const elProgressPercent = document.getElementById("progress-percent-text");
|
| 818 |
+
|
| 819 |
+
if(elProgressContainer) {
|
| 820 |
+
elProgressContainer.style.display = "block";
|
| 821 |
+
elProgressBar.value = 0;
|
| 822 |
+
elProgressStatus.textContent = "Starting...";
|
| 823 |
+
elProgressPercent.textContent = "0%";
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
// Build Backprop Config
|
| 827 |
+
const bpConfig = {
|
| 828 |
+
mode: elBpMode.value,
|
| 829 |
+
strategy: elBpStrategy.value,
|
| 830 |
+
k: parseInt(elBpK.value) || 10,
|
| 831 |
+
ref_token_id: parseInt(elBpRefId.value) || 0,
|
| 832 |
+
contrast_rank: 2 // simplified defaults
|
| 833 |
+
};
|
| 834 |
+
|
| 835 |
+
// Pruning Config
|
| 836 |
+
const pruningMode = elPruningMode ? elPruningMode.value : "by_per_layer_cum_mass_percentile";
|
| 837 |
+
const topP = elVisTopP ? parseFloat(elVisTopP.value) : 0.9;
|
| 838 |
+
const edgeThresh = elValGlobalThresh ? parseFloat(elValGlobalThresh.value) : 0.01;
|
| 839 |
+
|
| 840 |
+
try {
|
| 841 |
+
const response = await fetch(`${API_BASE}/compute_circuit`, {
|
| 842 |
+
method: 'POST',
|
| 843 |
+
headers: {'Content-Type': 'application/json'},
|
| 844 |
+
body: JSON.stringify({
|
| 845 |
+
backprop_config: bpConfig,
|
| 846 |
+
layers: layers,
|
| 847 |
+
pruning_mode: pruningMode,
|
| 848 |
+
top_p: topP,
|
| 849 |
+
edge_threshold: edgeThresh
|
| 850 |
+
})
|
| 851 |
+
});
|
| 852 |
+
|
| 853 |
+
if (!response.ok) {
|
| 854 |
+
// Try to parse error
|
| 855 |
+
let errMsg = "Server Error";
|
| 856 |
+
try {
|
| 857 |
+
const err = await response.json();
|
| 858 |
+
errMsg = err.detail || errMsg;
|
| 859 |
+
} catch(e) {}
|
| 860 |
+
throw new Error(errMsg);
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
// NDJSON Streaming Reader
|
| 864 |
+
const reader = response.body.getReader();
|
| 865 |
+
const decoder = new TextDecoder();
|
| 866 |
+
let buffer = '';
|
| 867 |
+
|
| 868 |
+
// Clear previous data
|
| 869 |
+
appState.circuitData = null;
|
| 870 |
+
|
| 871 |
+
while (true) {
|
| 872 |
+
const { value, done } = await reader.read();
|
| 873 |
+
if (done) break;
|
| 874 |
+
|
| 875 |
+
const chunk = decoder.decode(value, {stream: true});
|
| 876 |
+
buffer += chunk;
|
| 877 |
+
const lines = buffer.split('\n');
|
| 878 |
+
buffer = lines.pop(); // Keep partial
|
| 879 |
+
|
| 880 |
+
for (const line of lines) {
|
| 881 |
+
if (!line.trim()) continue;
|
| 882 |
+
try {
|
| 883 |
+
const msg = JSON.parse(line);
|
| 884 |
+
if (msg.type === "progress") {
|
| 885 |
+
if(elProgressContainer) {
|
| 886 |
+
const pct = Math.min(100, Math.max(0, msg.percent || 0));
|
| 887 |
+
elProgressBar.value = pct;
|
| 888 |
+
elProgressStatus.textContent = msg.msg;
|
| 889 |
+
elProgressPercent.textContent = Math.round(pct) + "%";
|
| 890 |
+
}
|
| 891 |
+
} else if (msg.type === "graph_data") {
|
| 892 |
+
// Received FULL graph
|
| 893 |
+
appState.circuitData = {
|
| 894 |
+
graph: msg.graph,
|
| 895 |
+
pruning_details: msg.pruning_details,
|
| 896 |
+
layers: layers // Or extract from graph nodes
|
| 897 |
+
};
|
| 898 |
+
appState.layout = null; // Clear cached layout
|
| 899 |
+
} else if (msg.type === "complete") {
|
| 900 |
+
if (appState.circuitData) {
|
| 901 |
+
requestAnimationFrame(() => updateCircuitLayout()); // directly to layout
|
| 902 |
+
}
|
| 903 |
+
} else if (msg.type === "error") {
|
| 904 |
+
throw new Error(msg.msg);
|
| 905 |
+
}
|
| 906 |
+
} catch (e) {
|
| 907 |
+
if (e instanceof SyntaxError) {
|
| 908 |
+
console.error("JSON Parse Error on line:", line, e);
|
| 909 |
+
continue;
|
| 910 |
+
}
|
| 911 |
+
throw e;
|
| 912 |
+
}
|
| 913 |
+
}
|
| 914 |
+
}
|
| 915 |
+
} catch (e) {
|
| 916 |
+
alert(e.message);
|
| 917 |
+
} finally {
|
| 918 |
+
elBtnCircuit.disabled = false;
|
| 919 |
+
elBtnCircuit.textContent = "Visualize Connection";
|
| 920 |
+
if(elProgressContainer) {
|
| 921 |
+
setTimeout(() => {
|
| 922 |
+
elProgressContainer.style.display = "none";
|
| 923 |
+
}, 2000);
|
| 924 |
+
}
|
| 925 |
+
}
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
function updateStatus(msg, type) {
|
| 929 |
+
elStatus.textContent = msg;
|
| 930 |
+
elStatus.className = `status ${type}`;
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
// --- Visualization Logic ---
|
| 934 |
+
|
| 935 |
+
// NEW: Separated heavy layout calculation from rendering
|
| 936 |
+
function updateCircuitLayout() {
|
| 937 |
+
if (!appState.circuitData || !appState.circuitData.graph) return;
|
| 938 |
+
showLoading("Updating Layout...");
|
| 939 |
+
|
| 940 |
+
// Yield to browser to render spinner
|
| 941 |
+
setTimeout(() => {
|
| 942 |
+
try {
|
| 943 |
+
_updateCircuitLayoutInternal();
|
| 944 |
+
} catch(e) {
|
| 945 |
+
console.error(e);
|
| 946 |
+
} finally {
|
| 947 |
+
hideLoading();
|
| 948 |
+
}
|
| 949 |
+
}, 50);
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
function _updateCircuitLayoutInternal() {
|
| 953 |
+
if (!appState.circuitData || !appState.circuitData.graph) return;
|
| 954 |
+
|
| 955 |
+
// Destructure graph early
|
| 956 |
+
const { graph, layers: requestedLayers } = appState.circuitData;
|
| 957 |
+
|
| 958 |
+
// Define robust accessors
|
| 959 |
+
// NetworkX alignment: v2 uses 'links', v3 uses 'edges'
|
| 960 |
+
const nodes = graph.nodes || [];
|
| 961 |
+
const edges = graph.edges || graph.links || [];
|
| 962 |
+
|
| 963 |
+
// DEBUG: Log Graph Data Structure
|
| 964 |
+
console.log("Graph Data Received:", graph);
|
| 965 |
+
if(nodes.length > 0) {
|
| 966 |
+
console.log("Sample Node:", nodes[0]);
|
| 967 |
+
}
|
| 968 |
+
if(edges.length > 0) {
|
| 969 |
+
console.log("Sample Link/Edge:", edges[0]);
|
| 970 |
+
} else {
|
| 971 |
+
console.log("No links/edges found. (Graph might be disconnected or threshold too high)");
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
// Check tokens
|
| 975 |
+
if (!appState.tokens || appState.tokens.length === 0) {
|
| 976 |
+
console.error("appState.tokens is missing! Cannot render graph. Please run Compute Logits first.");
|
| 977 |
+
alert("Error: Tokens missing. Please run 'Compute Logits' first.");
|
| 978 |
+
return;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
// Check toggle listeners
|
| 982 |
+
const toggle = document.getElementById('hide-bos-node');
|
| 983 |
+
if (toggle && !toggle.hasAttribute('data-listening')) {
|
| 984 |
+
toggle.addEventListener('change', updateCircuitLayout); // Need layout update to filter BOs
|
| 985 |
+
toggle.setAttribute('data-listening', 'true');
|
| 986 |
+
}
|
| 987 |
+
const showAllToggle = document.getElementById('show-all-tokens');
|
| 988 |
+
if (showAllToggle && !showAllToggle.hasAttribute('data-listening')) {
|
| 989 |
+
showAllToggle.addEventListener('change', updateCircuitLayout);
|
| 990 |
+
showAllToggle.setAttribute('data-listening', 'true');
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
const showNodeValuesToggle = document.getElementById('show-node-values');
|
| 994 |
+
if (showNodeValuesToggle && !showNodeValuesToggle.hasAttribute('data-listening')) {
|
| 995 |
+
showNodeValuesToggle.addEventListener('change', drawCircuit); // Visual only
|
| 996 |
+
showNodeValuesToggle.setAttribute('data-listening', 'true');
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
// const { graph, layers: requestedLayers } = appState.circuitData; // Moved up
|
| 1000 |
+
const tokens = appState.tokens;
|
| 1001 |
+
const layerHeight = parseFloat(elVisLayerSpacing ? elVisLayerSpacing.value : 300);
|
| 1002 |
+
|
| 1003 |
+
const fullSeqLen = tokens.length;
|
| 1004 |
+
const hideFirstToken = document.getElementById('hide-bos-node') && document.getElementById('hide-bos-node').checked;
|
| 1005 |
+
const showAllTokens = elShowAllTokens ? elShowAllTokens.checked : true;
|
| 1006 |
+
|
| 1007 |
+
// Data Slicing (if hiding BOS)
|
| 1008 |
+
let displaySeqLen = fullSeqLen;
|
| 1009 |
+
let displayTokens = tokens;
|
| 1010 |
+
let startTokenIdx = 0;
|
| 1011 |
+
|
| 1012 |
+
if (hideFirstToken && fullSeqLen > 1) {
|
| 1013 |
+
displayTokens = tokens.slice(1);
|
| 1014 |
+
displaySeqLen = fullSeqLen - 1;
|
| 1015 |
+
startTokenIdx = 1;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
// Process Graph Data
|
| 1019 |
+
// Nodes are { id: [layer, token], layer: L, token: T, relevance: R }
|
| 1020 |
+
// const nodes = graph.nodes; // Moved up
|
| 1021 |
+
// const edges = graph.links; // Moved up
|
| 1022 |
+
|
| 1023 |
+
// Identify Layers involved in graph
|
| 1024 |
+
// Use requestedLayers if available, or infer from graph
|
| 1025 |
+
let layersList = requestedLayers || [];
|
| 1026 |
+
if (layersList.length === 0) {
|
| 1027 |
+
const layersSet = new Set(nodes.map(n => n.layer));
|
| 1028 |
+
layersList = Array.from(layersSet).sort((a,b)=>a-b);
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
// Determine active nodes (those in graph)
|
| 1032 |
+
const activeNodeMap = new Map(); // key: "layer,token" -> nodeObj
|
| 1033 |
+
let globalMaxValRel = 0;
|
| 1034 |
+
let globalMaxValEdge = 0;
|
| 1035 |
+
|
| 1036 |
+
nodes.forEach(n => {
|
| 1037 |
+
// Filter BOS if needed
|
| 1038 |
+
if (hideFirstToken && n.token === 0) return; // Assume BOS is 0? Use n.token index check
|
| 1039 |
+
if (hideFirstToken && n.token < startTokenIdx) return;
|
| 1040 |
+
|
| 1041 |
+
const key = `${n.layer},${n.token}`;
|
| 1042 |
+
activeNodeMap.set(key, n);
|
| 1043 |
+
globalMaxValRel = Math.max(globalMaxValRel, Math.abs(n.relevance || 0));
|
| 1044 |
+
});
|
| 1045 |
+
|
| 1046 |
+
edges.forEach(e => {
|
| 1047 |
+
// e.source and e.target are arrays [L, T]
|
| 1048 |
+
const srcL = e.source[0];
|
| 1049 |
+
const srcT = e.source[1];
|
| 1050 |
+
const tgtL = e.target[0];
|
| 1051 |
+
const tgtT = e.target[1];
|
| 1052 |
+
|
| 1053 |
+
if (hideFirstToken && (srcT < startTokenIdx || tgtT < startTokenIdx)) return;
|
| 1054 |
+
|
| 1055 |
+
globalMaxValEdge = Math.max(globalMaxValEdge, Math.abs(e.weight || 0));
|
| 1056 |
+
});
|
| 1057 |
+
|
| 1058 |
+
const maxNorm = Math.max(globalMaxValRel, globalMaxValEdge, 1e-9);
|
| 1059 |
+
|
| 1060 |
+
// Layout Geometry Setup
|
| 1061 |
+
const nHops = layersList.length > 1 ? layersList.length - 1 : 1;
|
| 1062 |
+
const totalHeight = Math.max(500, nHops * layerHeight + 150);
|
| 1063 |
+
const margin = { top: 50, left: 120, right: 50, bottom: 50 };
|
| 1064 |
+
|
| 1065 |
+
|
| 1066 |
+
// Generate Layout Nodes
|
| 1067 |
+
const nodesByLayer = {};
|
| 1068 |
+
const allNodesFlat = [];
|
| 1069 |
+
const layerTotals = {};
|
| 1070 |
+
|
| 1071 |
+
// Helper to get active node data
|
| 1072 |
+
const getActiveData = (l, t) => activeNodeMap.get(`${l},${t}`);
|
| 1073 |
+
|
| 1074 |
+
// DYNAMIC LAYOUT LOGIC
|
| 1075 |
+
// 1. Identify "Active Columns": Any token index that participates in an EDGE.
|
| 1076 |
+
// This filters out columns that might have nodes but no connections (pruned).
|
| 1077 |
+
let activeTokenIndices = new Set();
|
| 1078 |
+
|
| 1079 |
+
// Use edges to find connected tokens
|
| 1080 |
+
edges.forEach(e => {
|
| 1081 |
+
// e.source/target are [layer, token]
|
| 1082 |
+
activeTokenIndices.add(e.source[1]);
|
| 1083 |
+
activeTokenIndices.add(e.target[1]);
|
| 1084 |
+
});
|
| 1085 |
+
|
| 1086 |
+
// Also include target node tokens?
|
| 1087 |
+
// Usually target is connected, but what if we have a top-p that prunes EVERYTHING?
|
| 1088 |
+
// We should probably show the target node column regardless, so the user sees where they started.
|
| 1089 |
+
// If backend prunes everything, we might have edges=[], nodes=[target].
|
| 1090 |
+
// Let's iterate nodes too, but check a flag or just rely on edges.
|
| 1091 |
+
// User Request: "hide the token positions whose nodes in all layers do not have any connections".
|
| 1092 |
+
// This implies strictly edges.
|
| 1093 |
+
// BUT: If the target node has no incoming edges (e.g. threshold too high), it is isolated.
|
| 1094 |
+
// Should we hide the target node? That seems confusing.
|
| 1095 |
+
// Let's prioritize edges, but maybe keep target?
|
| 1096 |
+
// For now, strict edge adherence based on user prompt.
|
| 1097 |
+
// If empty set (graph empty), we might show nothing or just full seq?
|
| 1098 |
+
if (activeTokenIndices.size === 0 && !showAllTokens && nodes.length > 0) {
|
| 1099 |
+
// Fallback: Show at least the nodes that exist (like target)
|
| 1100 |
+
nodes.forEach(n => activeTokenIndices.add(n.token));
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
// 2. Define "Render Columns"
|
| 1104 |
+
let renderColumns = [];
|
| 1105 |
+
if (showAllTokens) {
|
| 1106 |
+
// All tokens in display range
|
| 1107 |
+
for(let i=0; i<displaySeqLen; i++) renderColumns.push(startTokenIdx + i);
|
| 1108 |
+
} else {
|
| 1109 |
+
// Only active tokens
|
| 1110 |
+
renderColumns = Array.from(activeTokenIndices).sort((a,b) => a - b);
|
| 1111 |
+
// Filter by hideFirstToken if needed (activeNodeMap already filtered? No, activeNodeMap built from nodes list)
|
| 1112 |
+
// Ensure strictly respecting hideFirstToken setting
|
| 1113 |
+
if (hideFirstToken) {
|
| 1114 |
+
renderColumns = renderColumns.filter(t => t >= startTokenIdx);
|
| 1115 |
+
}
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
// 3. Spacing based on Render Columns count
|
| 1119 |
+
const nodesInRowForSpacing = renderColumns.length;
|
| 1120 |
+
const minW = nodesInRowForSpacing * (showAllTokens ? 15 : 30) + 100;
|
| 1121 |
+
const width = Math.max(800, minW);
|
| 1122 |
+
const drawingW = width - margin.left - margin.right;
|
| 1123 |
+
const nodeSpacing = drawingW / Math.max(1, nodesInRowForSpacing);
|
| 1124 |
+
|
| 1125 |
+
const yPositions = {};
|
| 1126 |
+
layersList.forEach((lIdx, i) => {
|
| 1127 |
+
yPositions[lIdx] = (totalHeight - margin.bottom) - (i * layerHeight);
|
| 1128 |
+
});
|
| 1129 |
+
|
| 1130 |
+
layersList.forEach((lIdx) => {
|
| 1131 |
+
const y = yPositions[lIdx];
|
| 1132 |
+
const rowNodeMap = {};
|
| 1133 |
+
let runningTotal = 0;
|
| 1134 |
+
|
| 1135 |
+
// Iterate Render Columns
|
| 1136 |
+
renderColumns.forEach((tokenIdx, visualColIdx) => {
|
| 1137 |
+
const activeData = getActiveData(lIdx, tokenIdx);
|
| 1138 |
+
const rel = activeData ? (activeData.relevance || 0) : 0;
|
| 1139 |
+
const normRel = Math.abs(rel) / maxNorm;
|
| 1140 |
+
|
| 1141 |
+
// accumulate total only if we want layer sum to reflect EVERYTHING or just visible?
|
| 1142 |
+
// Usually layer sum is total relevance. If we hide tokens, do we hide their relevance from sum?
|
| 1143 |
+
// Let's sum only active data found in graph.
|
| 1144 |
+
if (activeData) runningTotal += rel;
|
| 1145 |
+
|
| 1146 |
+
// X alignment: Based on visual column index
|
| 1147 |
+
const x = margin.left + visualColIdx * nodeSpacing + (nodeSpacing/2);
|
| 1148 |
+
|
| 1149 |
+
// Token String
|
| 1150 |
+
const rawToken = (tokenIdx < tokens.length) ? tokens[tokenIdx] : "?";
|
| 1151 |
+
const tokenStr = (typeof rawToken === 'string') ? rawToken : (rawToken.token_str || "?");
|
| 1152 |
+
|
| 1153 |
+
const node = {
|
| 1154 |
+
layer: lIdx,
|
| 1155 |
+
index: tokenIdx, // True token index
|
| 1156 |
+
visualIndex: visualColIdx, // For gap logic
|
| 1157 |
+
token: { token_str: tokenStr },
|
| 1158 |
+
x: x,
|
| 1159 |
+
y: y,
|
| 1160 |
+
rel: rel,
|
| 1161 |
+
normRel: normRel,
|
| 1162 |
+
isActive: !!activeData
|
| 1163 |
+
};
|
| 1164 |
+
allNodesFlat.push(node);
|
| 1165 |
+
rowNodeMap[tokenIdx] = node;
|
| 1166 |
+
});
|
| 1167 |
+
|
| 1168 |
+
nodesByLayer[lIdx] = rowNodeMap;
|
| 1169 |
+
layerTotals[lIdx] = runningTotal;
|
| 1170 |
+
});
|
| 1171 |
+
|
| 1172 |
+
// Generate Edges
|
| 1173 |
+
const visibleEdges = [];
|
| 1174 |
+
|
| 1175 |
+
edges.forEach(e => {
|
| 1176 |
+
const srcL = e.source[0];
|
| 1177 |
+
const srcT = e.source[1];
|
| 1178 |
+
const tgtL = e.target[0];
|
| 1179 |
+
const tgtT = e.target[1];
|
| 1180 |
+
|
| 1181 |
+
// Lookup in our generated layout nodes
|
| 1182 |
+
// nodesByLayer[layer][tokenIndex]
|
| 1183 |
+
if (!nodesByLayer[srcL] || !nodesByLayer[tgtL]) return;
|
| 1184 |
+
|
| 1185 |
+
const sNode = nodesByLayer[srcL][srcT];
|
| 1186 |
+
const tNode = nodesByLayer[tgtL][tgtT];
|
| 1187 |
+
|
| 1188 |
+
if (sNode && tNode) {
|
| 1189 |
+
const val = e.weight;
|
| 1190 |
+
const normVal = Math.abs(val) / maxNorm;
|
| 1191 |
+
|
| 1192 |
+
visibleEdges.push({
|
| 1193 |
+
source: sNode,
|
| 1194 |
+
target: tNode,
|
| 1195 |
+
val: val,
|
| 1196 |
+
normVal: normVal
|
| 1197 |
+
});
|
| 1198 |
+
}
|
| 1199 |
+
});
|
| 1200 |
+
|
| 1201 |
+
// STORE LAYOUT
|
| 1202 |
+
appState.layout = {
|
| 1203 |
+
width,
|
| 1204 |
+
totalHeight,
|
| 1205 |
+
margin,
|
| 1206 |
+
nodeSpacing,
|
| 1207 |
+
maxNorm,
|
| 1208 |
+
layersList,
|
| 1209 |
+
nodesByLayer,
|
| 1210 |
+
allNodesFlat,
|
| 1211 |
+
visibleEdges,
|
| 1212 |
+
activeIndicesList: [], // Not used in this sparse logic
|
| 1213 |
+
layerTotals,
|
| 1214 |
+
displaySeqLen,
|
| 1215 |
+
displayTokens, // Add this back for gap drawing
|
| 1216 |
+
displayConnections: [] // Legacy compat
|
| 1217 |
+
};
|
| 1218 |
+
|
| 1219 |
+
drawCircuit();
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
function drawCircuit() {
|
| 1223 |
+
if (!appState.layout) {
|
| 1224 |
+
// Init layout if data exists
|
| 1225 |
+
if(appState.circuitData) updateCircuitLayout();
|
| 1226 |
+
return;
|
| 1227 |
+
}
|
| 1228 |
+
|
| 1229 |
+
const L = appState.layout;
|
| 1230 |
+
|
| 1231 |
+
// Check Spacing Updates first (Geometry Recalc)
|
| 1232 |
+
const currentLayerHeight = parseFloat(elVisLayerSpacing ? elVisLayerSpacing.value : 300);
|
| 1233 |
+
// Use stored layers count for recalculating total height
|
| 1234 |
+
// We infer layersList from layout
|
| 1235 |
+
const nHops = L.displayConnections ? L.displayConnections.length : 1;
|
| 1236 |
+
let newTotalHeight = Math.max(500, nHops * currentLayerHeight + 150);
|
| 1237 |
+
|
| 1238 |
+
if (L.layersList && L.layersList.length > 0) {
|
| 1239 |
+
newTotalHeight = Math.max(500, (L.layersList.length - 1) * currentLayerHeight + 200);
|
| 1240 |
+
|
| 1241 |
+
// Pre-calculate Y for each layer (Optimization: O(Layers) instead of O(Nodes*Layers))
|
| 1242 |
+
const layerYMap = {};
|
| 1243 |
+
L.layersList.forEach((lid, idx) => {
|
| 1244 |
+
layerYMap[lid] = (newTotalHeight - L.margin.bottom) - (idx * currentLayerHeight);
|
| 1245 |
+
});
|
| 1246 |
+
|
| 1247 |
+
// Update Node Y Positions in place
|
| 1248 |
+
L.allNodesFlat.forEach(node => {
|
| 1249 |
+
if (layerYMap.hasOwnProperty(node.layer)) {
|
| 1250 |
+
node.y = layerYMap[node.layer];
|
| 1251 |
+
}
|
| 1252 |
+
});
|
| 1253 |
+
|
| 1254 |
+
// Edge coordinates update automatically since they reference node objects
|
| 1255 |
+
}
|
| 1256 |
+
L.totalHeight = newTotalHeight;
|
| 1257 |
+
|
| 1258 |
+
const ctx = elCanvas.getContext('2d');
|
| 1259 |
+
|
| 1260 |
+
// Check canvas dims
|
| 1261 |
+
if (elCanvas.width !== L.width || elCanvas.height !== L.totalHeight) {
|
| 1262 |
+
elCanvas.width = L.width;
|
| 1263 |
+
elCanvas.height = L.totalHeight;
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
+
// Vis Settings
|
| 1267 |
+
const strengthScale = parseFloat(elVisStrength.value);
|
| 1268 |
+
const showNodeValues = document.getElementById('show-node-values') ? document.getElementById('show-node-values').checked : true;
|
| 1269 |
+
const showAllTokens = elShowAllTokens ? elShowAllTokens.checked : true;
|
| 1270 |
+
const hideFirstToken = document.getElementById('hide-bos-node') && document.getElementById('hide-bos-node').checked;
|
| 1271 |
+
|
| 1272 |
+
// Add startTokenIdx logic here
|
| 1273 |
+
let startTokenIdx = 0;
|
| 1274 |
+
if (hideFirstToken && appState.tokens && appState.tokens.length > 1) {
|
| 1275 |
+
startTokenIdx = 1;
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
ctx.clearRect(0, 0, L.width, L.totalHeight);
|
| 1279 |
+
|
| 1280 |
+
// --- Interaction ---
|
| 1281 |
+
const mouseX = appState.mouseX || -1;
|
| 1282 |
+
const mouseY = appState.mouseY || -1;
|
| 1283 |
+
function checkHit(x, y) { return Math.sqrt((x-mouseX)**2 + (y-mouseY)**2) < 8; }
|
| 1284 |
+
|
| 1285 |
+
let hoveredNode = null;
|
| 1286 |
+
for (const n of L.allNodesFlat) {
|
| 1287 |
+
if (checkHit(n.x, n.y)) {
|
| 1288 |
+
hoveredNode = n;
|
| 1289 |
+
break;
|
| 1290 |
+
}
|
| 1291 |
+
}
|
| 1292 |
+
const activeNode = hoveredNode || selectedNode;
|
| 1293 |
+
const isHighlightActive = !!activeNode;
|
| 1294 |
+
|
| 1295 |
+
// --- Highlight Computation (One-Hop) ---
|
| 1296 |
+
const highlightedNodes = new Set();
|
| 1297 |
+
const highlightedEdges = new Set(); // store edges to paint bold
|
| 1298 |
+
|
| 1299 |
+
if (activeNode) {
|
| 1300 |
+
highlightedNodes.add(activeNode);
|
| 1301 |
+
|
| 1302 |
+
// Use visibleEdges directly
|
| 1303 |
+
L.visibleEdges.forEach(e => {
|
| 1304 |
+
// 1. Outgoing
|
| 1305 |
+
if (e.source === activeNode) {
|
| 1306 |
+
highlightedNodes.add(e.target);
|
| 1307 |
+
highlightedEdges.add(e);
|
| 1308 |
+
}
|
| 1309 |
+
// 2. Incoming
|
| 1310 |
+
if (e.target === activeNode) {
|
| 1311 |
+
highlightedNodes.add(e.source);
|
| 1312 |
+
highlightedEdges.add(e);
|
| 1313 |
+
}
|
| 1314 |
+
});
|
| 1315 |
+
}
|
| 1316 |
+
|
| 1317 |
+
// --- Draw Edges ---
|
| 1318 |
+
// Sort edges: Passive first, Active on top
|
| 1319 |
+
// Actually just draw active afterwards.
|
| 1320 |
+
|
| 1321 |
+
L.visibleEdges.forEach(e => {
|
| 1322 |
+
const isActive = isHighlightActive && highlightedEdges.has(e);
|
| 1323 |
+
const isDim = isHighlightActive && !isActive;
|
| 1324 |
+
|
| 1325 |
+
// If dim, maybe skip drawing very thin lines to save perf?
|
| 1326 |
+
// Or alpha.
|
| 1327 |
+
|
| 1328 |
+
let width = e.normVal * strengthScale;
|
| 1329 |
+
if (width < 0.5 && isDim) return; // Culling
|
| 1330 |
+
|
| 1331 |
+
width = Math.max(width, 0.5); // Minimal width for visibility
|
| 1332 |
+
|
| 1333 |
+
let alpha = isDim ? 0.1 : 1.0;
|
| 1334 |
+
if (isActive) {
|
| 1335 |
+
alpha = 1.0;
|
| 1336 |
+
width = Math.max(width, 1.0); // Boost active
|
| 1337 |
+
}
|
| 1338 |
+
|
| 1339 |
+
const color = e.val >= 0 ? `rgba(211, 47, 47, ${alpha})` : `rgba(25, 118, 210, ${alpha})`;
|
| 1340 |
+
|
| 1341 |
+
ctx.beginPath();
|
| 1342 |
+
ctx.moveTo(e.source.x, e.source.y);
|
| 1343 |
+
ctx.lineTo(e.target.x, e.target.y);
|
| 1344 |
+
ctx.strokeStyle = color;
|
| 1345 |
+
ctx.lineWidth = width;
|
| 1346 |
+
|
| 1347 |
+
// draw active later? No, simple batch is fine mostly.
|
| 1348 |
+
ctx.stroke();
|
| 1349 |
+
|
| 1350 |
+
if (isActive) { // Use isActive logic for edge label drawing
|
| 1351 |
+
// Draw Edge Label
|
| 1352 |
+
const midX = (e.source.x + e.target.x)/2;
|
| 1353 |
+
const midY = (e.source.y + e.target.y)/2;
|
| 1354 |
+
ctx.save();
|
| 1355 |
+
const dx = e.target.x - e.source.x;
|
| 1356 |
+
const dy = e.target.y - e.source.y;
|
| 1357 |
+
let angle = Math.atan2(dy, dx);
|
| 1358 |
+
if (Math.abs(angle) > Math.PI / 2) angle += Math.PI; // Correct text orientation
|
| 1359 |
+
|
| 1360 |
+
ctx.translate(midX, midY);
|
| 1361 |
+
ctx.rotate(angle);
|
| 1362 |
+
|
| 1363 |
+
ctx.font = 'bold 10px Arial';
|
| 1364 |
+
const text = e.val.toFixed(2);
|
| 1365 |
+
const metrics = ctx.measureText(text);
|
| 1366 |
+
const p = 2; // padding
|
| 1367 |
+
|
| 1368 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
|
| 1369 |
+
ctx.fillRect(-metrics.width/2 - p, -6, metrics.width + 2*p, 12);
|
| 1370 |
+
|
| 1371 |
+
ctx.fillStyle = e.val >= 0 ? '#b71c1c' : '#0d47a1';
|
| 1372 |
+
ctx.textAlign = 'center';
|
| 1373 |
+
ctx.textBaseline = 'middle';
|
| 1374 |
+
ctx.fillText(text, 0, 0);
|
| 1375 |
+
ctx.restore();
|
| 1376 |
+
}
|
| 1377 |
+
});
|
| 1378 |
+
|
| 1379 |
+
// --- Draw Nodes ---
|
| 1380 |
+
// Re-use Gap logic variables
|
| 1381 |
+
let prevIdx = -1;
|
| 1382 |
+
let listIdx = -1;
|
| 1383 |
+
const displayedGaps = [];
|
| 1384 |
+
const displayTokens = L.displayTokens;
|
| 1385 |
+
|
| 1386 |
+
L.allNodesFlat.forEach((node) => {
|
| 1387 |
+
// Logic for tracking gap indices per layer
|
| 1388 |
+
if (node.layer === L.layersList[0]) {
|
| 1389 |
+
listIdx++;
|
| 1390 |
+
} else {
|
| 1391 |
+
// Reset for other layers? Actually gap logic only applied to bottom layer in original code
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
const isSelfActive = activeNode === node;
|
| 1395 |
+
const isConnected = highlightedNodes.has(node) && !isSelfActive;
|
| 1396 |
+
const isInteractive = isHighlightActive;
|
| 1397 |
+
const isDim = isInteractive && !isSelfActive && !isConnected;
|
| 1398 |
+
|
| 1399 |
+
let radius = (node.normRel * strengthScale);
|
| 1400 |
+
radius = Math.max(radius, 2);
|
| 1401 |
+
if (isConnected) radius = Math.max(radius, strengthScale * 0.75);
|
| 1402 |
+
if (isSelfActive) radius = Math.max(radius, strengthScale * 1.25);
|
| 1403 |
+
|
| 1404 |
+
let fillStyle = '#e0e0e0';
|
| 1405 |
+
if (node.rel > 0.001) fillStyle = '#ef9a9a';
|
| 1406 |
+
else if (node.rel < -0.001) fillStyle = '#90caf9';
|
| 1407 |
+
else fillStyle = '#cfd8dc';
|
| 1408 |
+
|
| 1409 |
+
if (isHighlightActive && !isDim) {
|
| 1410 |
+
if (node.rel > 0.001) fillStyle = '#d32f2f';
|
| 1411 |
+
else if (node.rel < -0.001) fillStyle = '#1976d2';
|
| 1412 |
+
else fillStyle = '#455a64';
|
| 1413 |
+
}
|
| 1414 |
+
|
| 1415 |
+
if (isDim) {
|
| 1416 |
+
fillStyle = '#f5f5f5';
|
| 1417 |
+
radius = 3;
|
| 1418 |
+
}
|
| 1419 |
+
|
| 1420 |
+
ctx.beginPath();
|
| 1421 |
+
ctx.arc(node.x, node.y, radius, 0, 2*Math.PI);
|
| 1422 |
+
ctx.fillStyle = fillStyle;
|
| 1423 |
+
ctx.fill();
|
| 1424 |
+
|
| 1425 |
+
if (!isDim) {
|
| 1426 |
+
ctx.strokeStyle = (isSelfActive || isConnected) ? '#333' : '#bbb';
|
| 1427 |
+
ctx.lineWidth = 1;
|
| 1428 |
+
ctx.stroke();
|
| 1429 |
+
}
|
| 1430 |
+
|
| 1431 |
+
// Labels (Layer)
|
| 1432 |
+
// Check if node is the first visible node in its layer OR if it's the very first node of the sequence
|
| 1433 |
+
// We use activeIndicesList logic if available, otherwise just use "first in row" logic
|
| 1434 |
+
const isFirstInLayer = (node.index === 0) || (Object.values(L.nodesByLayer[node.layer])[0] === node);
|
| 1435 |
+
|
| 1436 |
+
if (isFirstInLayer) {
|
| 1437 |
+
ctx.fillStyle = '#2c3e50';
|
| 1438 |
+
ctx.font = 'bold 12px Arial';
|
| 1439 |
+
ctx.textAlign = 'right';
|
| 1440 |
+
|
| 1441 |
+
let label = "";
|
| 1442 |
+
if (Array.isArray(node.layer)) {
|
| 1443 |
+
label = `L${node.layer[0]} (${node.layer[1]})`;
|
| 1444 |
+
} else {
|
| 1445 |
+
label = node.layer === -1 ? "Embedding" : `Layer ${node.layer}`;
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
ctx.fillText(label, L.margin.left - 20, node.y + 4);
|
| 1449 |
+
const total = L.layerTotals[node.layer] || 0;
|
| 1450 |
+
ctx.font = '11px Arial';
|
| 1451 |
+
ctx.fillText(`Sum: ${total.toFixed(2)}`, L.margin.left - 20, node.y + 18);
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
+
// Labels (Tokens)
|
| 1455 |
+
const isBottomLayer = (node.layer === L.layersList[0]);
|
| 1456 |
+
let showToken = isBottomLayer;
|
| 1457 |
+
if (isHighlightActive && (isSelfActive || isConnected)) showToken = true;
|
| 1458 |
+
|
| 1459 |
+
if (showToken) {
|
| 1460 |
+
ctx.fillStyle = '#000';
|
| 1461 |
+
ctx.font = isSelfActive ? 'bold 11px Arial' : '10px Arial';
|
| 1462 |
+
ctx.textAlign = 'center';
|
| 1463 |
+
ctx.save();
|
| 1464 |
+
|
| 1465 |
+
if (isBottomLayer && node.layer !== L.layersList[L.layersList.length-1]) {
|
| 1466 |
+
ctx.translate(node.x, node.y + 15);
|
| 1467 |
+
ctx.rotate(Math.PI/4);
|
| 1468 |
+
} else {
|
| 1469 |
+
ctx.translate(node.x, node.y - 12);
|
| 1470 |
+
ctx.rotate(-Math.PI/4);
|
| 1471 |
+
}
|
| 1472 |
+
|
| 1473 |
+
let txt = formatTokenForDisplay(node.token.token_str, 'visual');
|
| 1474 |
+
ctx.fillText(txt, 0, 0);
|
| 1475 |
+
ctx.restore();
|
| 1476 |
+
|
| 1477 |
+
// Gap Marker Logic (Bottom Layer Only)
|
| 1478 |
+
if (isBottomLayer && !showAllTokens) {
|
| 1479 |
+
// Preceding Gap (Before first visible token)
|
| 1480 |
+
// If this is the first visible token (listIdx or visualIndex 0), check if there are hidden tokens before it
|
| 1481 |
+
const trueStartIdx = hideFirstToken ? startTokenIdx : 0;
|
| 1482 |
+
|
| 1483 |
+
if (node.visualIndex === 0 && node.index > trueStartIdx) {
|
| 1484 |
+
const gx = node.x - L.nodeSpacing/2;
|
| 1485 |
+
const gy = node.y + 45;
|
| 1486 |
+
const hidden = [];
|
| 1487 |
+
for(let k=trueStartIdx; k<node.index; k++) {
|
| 1488 |
+
if(displayTokens && k < displayTokens.length) {
|
| 1489 |
+
const tObj = displayTokens[k];
|
| 1490 |
+
const tStr = (typeof tObj === 'string') ? tObj : tObj.token_str;
|
| 1491 |
+
hidden.push(formatTokenForDisplay(tStr, 'visual'));
|
| 1492 |
+
}
|
| 1493 |
+
}
|
| 1494 |
+
if (hidden.length > 0) displayedGaps.push({x: gx, y: gy, hidden: hidden});
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
// Inter-token Gap
|
| 1498 |
+
// Previous Visible Node Index vs Current Node Index
|
| 1499 |
+
if (prevIdx !== -1 && (node.index - prevIdx) > 1) {
|
| 1500 |
+
// We can use visualIndex to backtrack to previous node X
|
| 1501 |
+
// node.visualIndex is current i. Previous was i-1.
|
| 1502 |
+
// xPrev = x of column (node.visualIndex - 1)
|
| 1503 |
+
const xPrev = node.x - L.nodeSpacing;
|
| 1504 |
+
|
| 1505 |
+
const gx = (xPrev + node.x)/2;
|
| 1506 |
+
const gy = node.y + 45;
|
| 1507 |
+
const hidden = [];
|
| 1508 |
+
for(let k=prevIdx+1; k<node.index; k++) {
|
| 1509 |
+
if(displayTokens && k < displayTokens.length) {
|
| 1510 |
+
const tObj = displayTokens[k];
|
| 1511 |
+
const tStr = (typeof tObj === 'string') ? tObj : tObj.token_str;
|
| 1512 |
+
hidden.push(formatTokenForDisplay(tStr, 'visual'));
|
| 1513 |
+
}
|
| 1514 |
+
}
|
| 1515 |
+
if (hidden.length > 0) displayedGaps.push({x: gx, y: gy, hidden: hidden});
|
| 1516 |
+
}
|
| 1517 |
+
prevIdx = node.index;
|
| 1518 |
+
}
|
| 1519 |
+
}
|
| 1520 |
+
|
| 1521 |
+
if (showNodeValues) {
|
| 1522 |
+
const valText = node.rel.toFixed(2);
|
| 1523 |
+
ctx.font = '9px Arial';
|
| 1524 |
+
ctx.fillStyle = node.rel >= 0 ? '#b71c1c' : '#0d47a1';
|
| 1525 |
+
ctx.textAlign = 'center';
|
| 1526 |
+
let textY = node.y - 14;
|
| 1527 |
+
if (!isBottomLayer) textY = node.y - 16;
|
| 1528 |
+
if (isSelfActive) ctx.font = 'bold 10px Arial';
|
| 1529 |
+
ctx.fillText(valText, node.x, textY);
|
| 1530 |
+
}
|
| 1531 |
+
});
|
| 1532 |
+
// --- Draw Gaps Loop ---
|
| 1533 |
+
let hoveredGap = null;
|
| 1534 |
+
// const { displayTokens } = L; // Already defined above
|
| 1535 |
+
|
| 1536 |
+
displayedGaps.forEach(gap => {
|
| 1537 |
+
const mx = appState.mouseX;
|
| 1538 |
+
const my = appState.mouseY;
|
| 1539 |
+
const dist = Math.sqrt(Math.pow(gap.x - mx, 2) + Math.pow(gap.y - my, 2));
|
| 1540 |
+
|
| 1541 |
+
if (dist < 15) hoveredGap = gap;
|
| 1542 |
+
|
| 1543 |
+
ctx.save();
|
| 1544 |
+
ctx.translate(gap.x, gap.y);
|
| 1545 |
+
ctx.fillStyle = (dist < 15) ? '#e0e0e0' : '#f5f5f5';
|
| 1546 |
+
ctx.strokeStyle = '#bdbdbd';
|
| 1547 |
+
ctx.lineWidth = 1;
|
| 1548 |
+
|
| 1549 |
+
ctx.beginPath();
|
| 1550 |
+
if(ctx.roundRect) ctx.roundRect(-12, -8, 24, 16, 4);
|
| 1551 |
+
else ctx.rect(-12, -8, 24, 16);
|
| 1552 |
+
ctx.fill();
|
| 1553 |
+
ctx.stroke();
|
| 1554 |
+
|
| 1555 |
+
ctx.fillStyle = '#616161';
|
| 1556 |
+
ctx.font = 'bold 12px Arial';
|
| 1557 |
+
ctx.textAlign = 'center';
|
| 1558 |
+
ctx.textBaseline = 'middle';
|
| 1559 |
+
ctx.fillText("...", 0, -2);
|
| 1560 |
+
ctx.restore();
|
| 1561 |
+
});
|
| 1562 |
+
|
| 1563 |
+
// --- Tooltip Updating ---
|
| 1564 |
+
const elTooltip = document.getElementById('tooltip');
|
| 1565 |
+
if (hoveredGap && elTooltip) {
|
| 1566 |
+
elTooltip.style.display = 'block';
|
| 1567 |
+
elTooltip.style.opacity = '1';
|
| 1568 |
+
|
| 1569 |
+
// Update content FIRST to measure size
|
| 1570 |
+
const fullText = hoveredGap.hidden ? hoveredGap.hidden.join(" ") : "";
|
| 1571 |
+
elTooltip.textContent = fullText || "[Empty]";
|
| 1572 |
+
elTooltip.style.maxWidth = "300px";
|
| 1573 |
+
|
| 1574 |
+
const rect = elCanvas.getBoundingClientRect();
|
| 1575 |
+
const gapX = rect.left + hoveredGap.x;
|
| 1576 |
+
const gapY = rect.top + hoveredGap.y;
|
| 1577 |
+
|
| 1578 |
+
// Default: Bottom-Right
|
| 1579 |
+
let finalLeft = gapX + 10;
|
| 1580 |
+
let finalTop = gapY + 10;
|
| 1581 |
+
|
| 1582 |
+
// Check Bounds
|
| 1583 |
+
const tooltipRect = elTooltip.getBoundingClientRect();
|
| 1584 |
+
|
| 1585 |
+
// Vertical overflow (flip up)
|
| 1586 |
+
if (finalTop + tooltipRect.height > window.innerHeight) {
|
| 1587 |
+
finalTop = gapY - tooltipRect.height - 10;
|
| 1588 |
+
}
|
| 1589 |
+
|
| 1590 |
+
// Horizontal overflow (flip left)
|
| 1591 |
+
if (finalLeft + tooltipRect.width > window.innerWidth) {
|
| 1592 |
+
finalLeft = gapX - tooltipRect.width - 10;
|
| 1593 |
+
}
|
| 1594 |
+
|
| 1595 |
+
elTooltip.style.left = finalLeft + 'px';
|
| 1596 |
+
elTooltip.style.top = finalTop + 'px';
|
| 1597 |
+
|
| 1598 |
+
} else if (elTooltip) {
|
| 1599 |
+
elTooltip.style.display = 'none';
|
| 1600 |
+
elTooltip.style.opacity = '0';
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
// Mouse Interaction Tracking
|
| 1604 |
+
// Note: Re-binding these every draw call is inefficient but matches original logic structure.
|
| 1605 |
+
// Ideally move these out of drawCircuit.
|
| 1606 |
+
elCanvas.onmousemove = function(e) {
|
| 1607 |
+
const rect = elCanvas.getBoundingClientRect();
|
| 1608 |
+
appState.mouseX = e.clientX - rect.left;
|
| 1609 |
+
appState.mouseY = e.clientY - rect.top;
|
| 1610 |
+
requestAnimationFrame(drawCircuit);
|
| 1611 |
+
};
|
| 1612 |
+
|
| 1613 |
+
elCanvas.onmouseleave = function() {
|
| 1614 |
+
appState.mouseX = -1;
|
| 1615 |
+
appState.mouseY = -1;
|
| 1616 |
+
requestAnimationFrame(drawCircuit);
|
| 1617 |
+
};
|
| 1618 |
+
|
| 1619 |
+
elCanvas.onclick = function(e) {
|
| 1620 |
+
const rect = elCanvas.getBoundingClientRect();
|
| 1621 |
+
const x = e.clientX - rect.left;
|
| 1622 |
+
const y = e.clientY - rect.top;
|
| 1623 |
+
|
| 1624 |
+
let hit = null;
|
| 1625 |
+
if(L && L.allNodesFlat) {
|
| 1626 |
+
for (const n of L.allNodesFlat) {
|
| 1627 |
+
if (Math.sqrt(Math.pow(x - n.x, 2) + Math.pow(y - n.y, 2)) < 10) {
|
| 1628 |
+
hit = n;
|
| 1629 |
+
break;
|
| 1630 |
+
}
|
| 1631 |
+
}
|
| 1632 |
+
}
|
| 1633 |
+
|
| 1634 |
+
if (hit) {
|
| 1635 |
+
if (activeNode && activeNode.layer === hit.layer && activeNode.index === hit.index && !hoveredNode) {
|
| 1636 |
+
selectedNode = null;
|
| 1637 |
+
} else {
|
| 1638 |
+
selectedNode = hit;
|
| 1639 |
+
}
|
| 1640 |
+
} else {
|
| 1641 |
+
selectedNode = null;
|
| 1642 |
+
}
|
| 1643 |
+
requestAnimationFrame(drawCircuit);
|
| 1644 |
+
};
|
| 1645 |
+
}
|
| 1646 |
+
|
| 1647 |
+
// Initialization and Cleanup
|
| 1648 |
+
window.addEventListener('DOMContentLoaded', async () => {
|
| 1649 |
+
// Call cleanup endpoint to release GPU memory on page refresh
|
| 1650 |
+
try {
|
| 1651 |
+
await fetch(`${API_BASE}/cleanup`, { method: 'POST' });
|
| 1652 |
+
console.log("Backend memory cleaned up.");
|
| 1653 |
+
} catch (e) {
|
| 1654 |
+
console.warn("Failed to cleanup backend memory:", e);
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
+
// Init Datasets
|
| 1658 |
+
await fetchDatasets();
|
| 1659 |
+
});
|
| 1660 |
+
|
| 1661 |
+
// --- Trace Loading Logic ---
|
| 1662 |
+
|
| 1663 |
+
async function fetchDatasets() {
|
| 1664 |
+
try {
|
| 1665 |
+
const resp = await fetch(`${API_BASE}/datasets`);
|
| 1666 |
+
const data = await resp.json();
|
| 1667 |
+
|
| 1668 |
+
elTraceDataset.innerHTML = '<option value="">-- Select --</option>';
|
| 1669 |
+
data.datasets.forEach(ds => {
|
| 1670 |
+
const opt = document.createElement('option');
|
| 1671 |
+
opt.value = ds;
|
| 1672 |
+
opt.textContent = ds;
|
| 1673 |
+
elTraceDataset.appendChild(opt);
|
| 1674 |
+
});
|
| 1675 |
+
} catch (e) {
|
| 1676 |
+
console.error("Failed to fetch datasets:", e);
|
| 1677 |
+
}
|
| 1678 |
+
}
|
| 1679 |
+
|
| 1680 |
+
async function fetchTraces(dataset) {
|
| 1681 |
+
elTraceFile.disabled = true;
|
| 1682 |
+
elTraceFile.innerHTML = '<option value="">Loading...</option>';
|
| 1683 |
+
showLoading("Fetching Traces...");
|
| 1684 |
+
|
| 1685 |
+
try {
|
| 1686 |
+
const resp = await fetch(`${API_BASE}/traces/${dataset}`);
|
| 1687 |
+
if (!resp.ok) throw new Error("Failed");
|
| 1688 |
+
|
| 1689 |
+
const data = await resp.json();
|
| 1690 |
+
|
| 1691 |
+
elTraceFile.innerHTML = '<option value="">-- Select Trace --</option>';
|
| 1692 |
+
data.traces.forEach(t => {
|
| 1693 |
+
const opt = document.createElement('option');
|
| 1694 |
+
opt.value = t;
|
| 1695 |
+
opt.textContent = t;
|
| 1696 |
+
elTraceFile.appendChild(opt);
|
| 1697 |
+
});
|
| 1698 |
+
elTraceFile.disabled = false;
|
| 1699 |
+
} catch (e) {
|
| 1700 |
+
console.error("Failed to fetch traces:", e);
|
| 1701 |
+
elTraceFile.innerHTML = '<option value="">Error</option>';
|
| 1702 |
+
} finally {
|
| 1703 |
+
hideLoading();
|
| 1704 |
+
}
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
async function loadTraceDetails(dataset, traceId) {
|
| 1708 |
+
showLoading("Loading Trace Config...");
|
| 1709 |
+
try {
|
| 1710 |
+
const resp = await fetch(`${API_BASE}/trace_details/${dataset}/${traceId}`);
|
| 1711 |
+
if (!resp.ok) throw new Error("Failed");
|
| 1712 |
+
|
| 1713 |
+
const data = await resp.json();
|
| 1714 |
+
|
| 1715 |
+
// Update UI
|
| 1716 |
+
if(data.model_path) elModelPath.value = data.model_path;
|
| 1717 |
+
if(data.prompt) elPrompt.value = data.prompt;
|
| 1718 |
+
|
| 1719 |
+
// Populate "Prompt + Original Completion"
|
| 1720 |
+
if(elPromptOrig) {
|
| 1721 |
+
// Prefer raw_prompt if available (added to backend), else prompt (which might be full concat)
|
| 1722 |
+
const basePrompt = data.raw_prompt !== undefined ? data.raw_prompt : (data.prompt || "");
|
| 1723 |
+
elPromptOrig.value = basePrompt + (data.completion || "");
|
| 1724 |
+
}
|
| 1725 |
+
|
| 1726 |
+
// Set Config Defaults as requested
|
| 1727 |
+
elQuant.checked = data.quantization; // Default false
|
| 1728 |
+
elModelDtype.value = data.dtype || "bfloat16";
|
| 1729 |
+
|
| 1730 |
+
// Determine "Other" candidates data
|
| 1731 |
+
let otherData = null;
|
| 1732 |
+
let otherLabel = "Other Model Candidates";
|
| 1733 |
+
|
| 1734 |
+
if (data.other_candidates && Object.keys(data.other_candidates).length > 0) {
|
| 1735 |
+
// Pick first likely candidate key
|
| 1736 |
+
const keys = Object.keys(data.other_candidates);
|
| 1737 |
+
const key = keys[0];
|
| 1738 |
+
otherData = data.other_candidates[key];
|
| 1739 |
+
|
| 1740 |
+
// Heuristic for label
|
| 1741 |
+
if (key === '4b') otherLabel = "Qwen3-4B Candidates";
|
| 1742 |
+
else if (key === '1.7b' || key === '1_7b') otherLabel = "Qwen3-1.7B Candidates";
|
| 1743 |
+
else if (key.toLowerCase().includes('qwen')) otherLabel = key;
|
| 1744 |
+
else otherLabel = key + " Candidates";
|
| 1745 |
+
|
| 1746 |
+
} else if (data.topk_token_explore_4b && data.topk_token_explore_4b.length) {
|
| 1747 |
+
// Legacy Fallback
|
| 1748 |
+
otherData = data.topk_token_explore_4b;
|
| 1749 |
+
otherLabel = "Qwen3-4B Candidates";
|
| 1750 |
+
}
|
| 1751 |
+
|
| 1752 |
+
// Render Exploration Data if available
|
| 1753 |
+
if (elSectionTraceExplore) {
|
| 1754 |
+
let hasContent = false;
|
| 1755 |
+
|
| 1756 |
+
if (data.topk_token_explore && data.topk_token_explore.length) {
|
| 1757 |
+
renderExploreTable(elContainerExploreOriginal, data.topk_token_explore);
|
| 1758 |
+
hasContent = true;
|
| 1759 |
+
} else {
|
| 1760 |
+
elContainerExploreOriginal.innerHTML = "<div style='padding:10px;'>No data</div>";
|
| 1761 |
+
}
|
| 1762 |
+
|
| 1763 |
+
if (otherData && otherData.length) {
|
| 1764 |
+
renderExploreTable(elContainerExplore4b, otherData);
|
| 1765 |
+
if (elHeaderExploreOther) elHeaderExploreOther.textContent = otherLabel;
|
| 1766 |
+
hasContent = true;
|
| 1767 |
+
} else {
|
| 1768 |
+
elContainerExplore4b.innerHTML = "<div style='padding:10px; color:#666;'>No exploration data available.</div>";
|
| 1769 |
+
if (elHeaderExploreOther) elHeaderExploreOther.textContent = otherLabel;
|
| 1770 |
+
}
|
| 1771 |
+
|
| 1772 |
+
if (hasContent) {
|
| 1773 |
+
elSectionTraceExplore.classList.remove('hidden');
|
| 1774 |
+
} else {
|
| 1775 |
+
elSectionTraceExplore.classList.add('hidden');
|
| 1776 |
+
}
|
| 1777 |
+
}
|
| 1778 |
+
|
| 1779 |
+
// Flash success?
|
| 1780 |
+
console.log("Trace loaded:", data);
|
| 1781 |
+
} catch (e) {
|
| 1782 |
+
alert("Failed to load trace details: " + e.message);
|
| 1783 |
+
} finally {
|
| 1784 |
+
hideLoading();
|
| 1785 |
+
}
|
| 1786 |
+
}
|
| 1787 |
+
|
| 1788 |
+
// Init Trace Listeners
|
| 1789 |
+
if (elTraceDataset) {
|
| 1790 |
+
elTraceDataset.addEventListener('change', (e) => {
|
| 1791 |
+
const ds = e.target.value;
|
| 1792 |
+
if (ds) {
|
| 1793 |
+
fetchTraces(ds);
|
| 1794 |
+
} else {
|
| 1795 |
+
elTraceFile.innerHTML = '<option value="">-- Select Dataset First --</option>';
|
| 1796 |
+
elTraceFile.disabled = true;
|
| 1797 |
+
}
|
| 1798 |
+
});
|
| 1799 |
+
}
|
| 1800 |
+
|
| 1801 |
+
if (elTraceFile) {
|
| 1802 |
+
elTraceFile.addEventListener('change', (e) => {
|
| 1803 |
+
const t = e.target.value;
|
| 1804 |
+
const ds = elTraceDataset.value;
|
| 1805 |
+
if (t && ds) {
|
| 1806 |
+
loadTraceDetails(ds, t);
|
| 1807 |
+
}
|
| 1808 |
+
});
|
| 1809 |
+
}
|
| 1810 |
+
// ---------------------------
|
| 1811 |
+
|
| 1812 |
+
function generateLayerPresets() {
|
| 1813 |
+
// Helper update
|
| 1814 |
+
}
|
| 1815 |
+
|
| 1816 |
+
function setLayerPreset(mode) {
|
| 1817 |
+
const N = appState.n_layers || 28;
|
| 1818 |
+
let layers = [];
|
| 1819 |
+
if (mode === 'all') {
|
| 1820 |
+
layers.push(-1);
|
| 1821 |
+
for(let i=0; i<N; i++) layers.push(i);
|
| 1822 |
+
} else {
|
| 1823 |
+
// Default: 5 parts
|
| 1824 |
+
// 0, N/4, 2N/4, 3N/4, N-1
|
| 1825 |
+
const steps = 5;
|
| 1826 |
+
const stepSize = (N - 1) / steps;
|
| 1827 |
+
const set = new Set();
|
| 1828 |
+
set.add(-1); // Always include Embedding as requested
|
| 1829 |
+
for(let i=0; i<=steps; i++) {
|
| 1830 |
+
set.add(Math.round(i * stepSize));
|
| 1831 |
+
}
|
| 1832 |
+
layers = Array.from(set).sort((a,b)=>a-b);
|
| 1833 |
+
}
|
| 1834 |
+
elLayersList.value = layers.join(', ');
|
| 1835 |
+
}
|
| 1836 |
+
|
| 1837 |
+
// FORMAT HELPER
|
| 1838 |
+
function formatTokenForDisplay(tokenStr, escapeMode='visual') {
|
| 1839 |
+
if (!tokenStr) return tokenStr;
|
| 1840 |
+
|
| 1841 |
+
// 1. Escaping for Data Attributes or Generic Log usage
|
| 1842 |
+
if (escapeMode === 'data') {
|
| 1843 |
+
return tokenStr.replace(/\\/g, '\\\\')
|
| 1844 |
+
.replace(/'/g, "\\'")
|
| 1845 |
+
.replace(/\n/g, '\\n')
|
| 1846 |
+
.replace(/\r/g, '\\r')
|
| 1847 |
+
.replace(/"/g, '"');
|
| 1848 |
+
}
|
| 1849 |
+
|
| 1850 |
+
// 2. Visual Display (Visible \n)
|
| 1851 |
+
if (escapeMode === 'visual') {
|
| 1852 |
+
if (tokenStr === '\n') return '\\n';
|
| 1853 |
+
if (tokenStr === '\n\n') return '\\n\\n';
|
| 1854 |
+
if (tokenStr === '\r\n') return '\\r\\n';
|
| 1855 |
+
|
| 1856 |
+
// Check for mixed content
|
| 1857 |
+
if (tokenStr.trim() === '' && tokenStr.length > 0) {
|
| 1858 |
+
// It is whitespace
|
| 1859 |
+
if (tokenStr === ' ') return '␣'; // Optional
|
| 1860 |
+
}
|
| 1861 |
+
|
| 1862 |
+
// If containing newlines mixed with text, escape the newlines
|
| 1863 |
+
if (tokenStr.includes('\n')) return tokenStr.replace(/\n/g, '\\n');
|
| 1864 |
+
|
| 1865 |
+
return tokenStr;
|
| 1866 |
+
}
|
| 1867 |
+
|
| 1868 |
+
return tokenStr;
|
| 1869 |
+
}
|
| 1870 |
+
|
| 1871 |
+
// Initialize Preset on Load Model success (or when appState.n_layers is known)
|
| 1872 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1873 |
+
setLayerPreset('default');
|
| 1874 |
+
});
|
| 1875 |
+
|
| 1876 |
+
|
| 1877 |
+
function renderExploreTable(container, data) {
|
| 1878 |
+
if (!container) return;
|
| 1879 |
+
|
| 1880 |
+
if (!data || data.length === 0) {
|
| 1881 |
+
container.innerHTML = '<div style="padding:10px; color: #888;">No exploration data available.</div>';
|
| 1882 |
+
return;
|
| 1883 |
+
}
|
| 1884 |
+
|
| 1885 |
+
let html = `
|
| 1886 |
+
<table style="width: 100%; border-collapse: collapse; font-size: 0.85em;">
|
| 1887 |
+
<thead>
|
| 1888 |
+
<tr style="background: #f8f8f8; text-align: left; position: sticky; top: 0; z-index: 10;">
|
| 1889 |
+
<th style="padding: 6px; border-bottom: 2px solid #ddd;">Rank</th>
|
| 1890 |
+
<th style="padding: 6px; border-bottom: 2px solid #ddd;">Token</th>
|
| 1891 |
+
<th style="padding: 6px; border-bottom: 2px solid #ddd;">Logit</th>
|
| 1892 |
+
<th style="padding: 6px; border-bottom: 2px solid #ddd;">Res</th>
|
| 1893 |
+
<th style="padding: 6px; border-bottom: 2px solid #ddd;">Completion Start</th>
|
| 1894 |
+
</tr>
|
| 1895 |
+
</thead>
|
| 1896 |
+
<tbody>
|
| 1897 |
+
`;
|
| 1898 |
+
|
| 1899 |
+
data.forEach(item => {
|
| 1900 |
+
const isCorrect = item.eval_result === true;
|
| 1901 |
+
const correctColor = isCorrect ? '#2e7d32' : '#c62828';
|
| 1902 |
+
const correctBg = isCorrect ? '#e8f5e9' : '#ffebee';
|
| 1903 |
+
const correctIcon = isCorrect ? 'OK' : 'Fail';
|
| 1904 |
+
|
| 1905 |
+
let tokenStr = item.token_str || "";
|
| 1906 |
+
// Use the existing formatTokenForDisplay helper if available, or simple replacement
|
| 1907 |
+
let displayToken = tokenStr;
|
| 1908 |
+
if (typeof formatTokenForDisplay === 'function') {
|
| 1909 |
+
displayToken = formatTokenForDisplay(tokenStr);
|
| 1910 |
+
} else {
|
| 1911 |
+
displayToken = tokenStr.replace(/Ġ/g, ' ').replace(/Ċ/g, '\n');
|
| 1912 |
+
}
|
| 1913 |
+
|
| 1914 |
+
// Truncate completion
|
| 1915 |
+
let rawCompletion = item.completion || "";
|
| 1916 |
+
let displayCompletion = rawCompletion;
|
| 1917 |
+
|
| 1918 |
+
const maxLen = 60;
|
| 1919 |
+
if (displayCompletion.length > maxLen) {
|
| 1920 |
+
displayCompletion = displayCompletion.substring(0, maxLen) + "...";
|
| 1921 |
+
}
|
| 1922 |
+
|
| 1923 |
+
// Escape HTML
|
| 1924 |
+
const escapeHtml = (text) => {
|
| 1925 |
+
return text
|
| 1926 |
+
.replace(/&/g, "&")
|
| 1927 |
+
.replace(/</g, "<")
|
| 1928 |
+
.replace(/>/g, ">")
|
| 1929 |
+
.replace(/"/g, """)
|
| 1930 |
+
.replace(/'/g, "'");
|
| 1931 |
+
};
|
| 1932 |
+
|
| 1933 |
+
const fullCompletionEscaped = escapeHtml(rawCompletion);
|
| 1934 |
+
const displayCompletionEscaped = escapeHtml(displayCompletion);
|
| 1935 |
+
|
| 1936 |
+
html += `
|
| 1937 |
+
<tr style="border-bottom: 1px solid #eee;">
|
| 1938 |
+
<td style="padding: 4px 6px;">${item.rank}</td>
|
| 1939 |
+
<td style="padding: 4px 6px; font-family: monospace; background: #fafafa;">${displayToken}</td>
|
| 1940 |
+
<td style="padding: 4px 6px;">${(item.logits || 0).toFixed(2)}</td>
|
| 1941 |
+
<td style="padding: 4px 6px;">
|
| 1942 |
+
<span style="font-size: 0.8em; padding: 2px 4px; border-radius: 4px; background: ${correctBg}; color: ${correctColor}; font-weight: bold;">
|
| 1943 |
+
${correctIcon}
|
| 1944 |
+
</span>
|
| 1945 |
+
</td>
|
| 1946 |
+
<td style="padding: 4px 6px; color: #555; font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px;" title="${fullCompletionEscaped}">${displayCompletionEscaped}</td>
|
| 1947 |
+
</tr>
|
| 1948 |
+
`;
|
| 1949 |
+
});
|
| 1950 |
+
|
| 1951 |
+
html += '</tbody></table>';
|
| 1952 |
+
container.innerHTML = html;
|
| 1953 |
+
}
|
| 1954 |
+
|
| 1955 |
+
/* MODEL SELECTION LOGIC */
|
| 1956 |
+
async function initModelSelectors() {
|
| 1957 |
+
if(!elModelSeries || !elModelSelect) return;
|
| 1958 |
+
|
| 1959 |
+
// Series change handler
|
| 1960 |
+
elModelSeries.addEventListener('change', async () => {
|
| 1961 |
+
const series = elModelSeries.value;
|
| 1962 |
+
elModelSelect.innerHTML = "<option>Loading...</option>";
|
| 1963 |
+
elModelSelect.disabled = true;
|
| 1964 |
+
|
| 1965 |
+
try {
|
| 1966 |
+
const res = await fetch(`${API_BASE}/list_hf_models?series=${series}`);
|
| 1967 |
+
const data = await res.json();
|
| 1968 |
+
|
| 1969 |
+
elModelSelect.innerHTML = "";
|
| 1970 |
+
if(data.models && data.models.length > 0) {
|
| 1971 |
+
// Sort models alphabetically
|
| 1972 |
+
data.models.sort();
|
| 1973 |
+
data.models.forEach(modelName => {
|
| 1974 |
+
const opt = document.createElement("option");
|
| 1975 |
+
opt.value = modelName;
|
| 1976 |
+
opt.textContent = modelName;
|
| 1977 |
+
elModelSelect.appendChild(opt);
|
| 1978 |
+
});
|
| 1979 |
+
// Select first
|
| 1980 |
+
elModelSelect.disabled = false;
|
| 1981 |
+
elModelSelect.dispatchEvent(new Event('change'));
|
| 1982 |
+
} else {
|
| 1983 |
+
elModelSelect.innerHTML = "<option>No models found</option>";
|
| 1984 |
+
}
|
| 1985 |
+
} catch(e) {
|
| 1986 |
+
console.error("Failed to list models", e);
|
| 1987 |
+
elModelSelect.innerHTML = "<option>Error loading list</option>";
|
| 1988 |
+
}
|
| 1989 |
+
});
|
| 1990 |
+
|
| 1991 |
+
// Model change handler
|
| 1992 |
+
elModelSelect.addEventListener('change', async () => {
|
| 1993 |
+
const modelName = elModelSelect.value;
|
| 1994 |
+
if(!modelName || modelName.includes("Loading")) return;
|
| 1995 |
+
|
| 1996 |
+
// Update Path Input
|
| 1997 |
+
elModelPath.value = modelName;
|
| 1998 |
+
|
| 1999 |
+
// Update Revisions
|
| 2000 |
+
elModelRevision.innerHTML = '<option>Loading...</option>';
|
| 2001 |
+
elModelRevision.disabled = true;
|
| 2002 |
+
|
| 2003 |
+
try {
|
| 2004 |
+
const res = await fetch(`${API_BASE}/list_model_revisions?model_id=${encodeURIComponent(modelName)}`);
|
| 2005 |
+
const data = await res.json();
|
| 2006 |
+
|
| 2007 |
+
elModelRevision.innerHTML = '<option value="">Latest (Default)</option>';
|
| 2008 |
+
|
| 2009 |
+
const allRevs = [...(data.branches || []), ...(data.tags || [])];
|
| 2010 |
+
|
| 2011 |
+
if(allRevs.length > 0) {
|
| 2012 |
+
// Sort revisions alphabetically
|
| 2013 |
+
allRevs.sort();
|
| 2014 |
+
allRevs.forEach(rev => {
|
| 2015 |
+
if (rev === 'main') return; // Skip main as it is usually default
|
| 2016 |
+
const opt = document.createElement("option");
|
| 2017 |
+
opt.value = rev;
|
| 2018 |
+
opt.textContent = rev;
|
| 2019 |
+
elModelRevision.appendChild(opt);
|
| 2020 |
+
});
|
| 2021 |
+
}
|
| 2022 |
+
elModelRevision.disabled = false;
|
| 2023 |
+
} catch (e) {
|
| 2024 |
+
console.error("Failed to list revisions", e);
|
| 2025 |
+
elModelRevision.innerHTML = '<option value="">Latest (Default)</option>';
|
| 2026 |
+
elModelRevision.disabled = false;
|
| 2027 |
+
}
|
| 2028 |
+
});
|
| 2029 |
+
|
| 2030 |
+
// Trigger initial population
|
| 2031 |
+
elModelSeries.dispatchEvent(new Event('change'));
|
| 2032 |
+
}
|
| 2033 |
+
console.log("Initializing model selectors...");
|
| 2034 |
+
|
| 2035 |
+
function saveCircuitGraph() {
|
| 2036 |
+
if (!elCanvas) return;
|
| 2037 |
+
|
| 2038 |
+
// Create a temporary link
|
| 2039 |
+
const link = document.createElement('a');
|
| 2040 |
+
|
| 2041 |
+
// Generate filename with timestamp
|
| 2042 |
+
const date = new Date();
|
| 2043 |
+
const timestamp = date.toISOString().replace(/[:.]/g, '-');
|
| 2044 |
+
link.download = `circuit_graph_${timestamp}.png`;
|
| 2045 |
+
|
| 2046 |
+
// Convert canvas to blob/dataURL
|
| 2047 |
+
// High quality PNG
|
| 2048 |
+
const dataUrl = elCanvas.toDataURL('image/png', 1.0);
|
| 2049 |
+
|
| 2050 |
+
link.href = dataUrl;
|
| 2051 |
+
document.body.appendChild(link);
|
| 2052 |
+
link.click();
|
| 2053 |
+
document.body.removeChild(link);
|
| 2054 |
+
}
|
| 2055 |
+
|
| 2056 |
+
function saveCircuitGraphPDF() {
|
| 2057 |
+
if (!elCanvas) return;
|
| 2058 |
+
|
| 2059 |
+
try {
|
| 2060 |
+
const { jsPDF } = window.jspdf;
|
| 2061 |
+
|
| 2062 |
+
// Create temp canvas to flatten background
|
| 2063 |
+
const tempCanvas = document.createElement('canvas');
|
| 2064 |
+
tempCanvas.width = elCanvas.width;
|
| 2065 |
+
tempCanvas.height = elCanvas.height;
|
| 2066 |
+
const tCtx = tempCanvas.getContext('2d');
|
| 2067 |
+
|
| 2068 |
+
// Fill white
|
| 2069 |
+
tCtx.fillStyle = '#ffffff';
|
| 2070 |
+
tCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
| 2071 |
+
|
| 2072 |
+
// Draw original
|
| 2073 |
+
tCtx.drawImage(elCanvas, 0, 0);
|
| 2074 |
+
|
| 2075 |
+
// Calculate PDF size
|
| 2076 |
+
// Orientation based on aspect ratio
|
| 2077 |
+
const orientation = (elCanvas.width > elCanvas.height) ? 'l' : 'p';
|
| 2078 |
+
|
| 2079 |
+
const doc = new jsPDF({
|
| 2080 |
+
orientation: orientation,
|
| 2081 |
+
unit: 'px',
|
| 2082 |
+
format: [elCanvas.width, elCanvas.height] // Custom size matching canvas
|
| 2083 |
+
});
|
| 2084 |
+
|
| 2085 |
+
const imgData = tempCanvas.toDataURL('image/jpeg', 1.0);
|
| 2086 |
+
|
| 2087 |
+
doc.addImage(imgData, 'JPEG', 0, 0, elCanvas.width, elCanvas.height);
|
| 2088 |
+
|
| 2089 |
+
const date = new Date();
|
| 2090 |
+
const timestamp = date.toISOString().replace(/[:.]/g, '-');
|
| 2091 |
+
doc.save(`circuit_graph_${timestamp}.pdf`);
|
| 2092 |
+
} catch (e) {
|
| 2093 |
+
console.error("PDF generation failed:", e);
|
| 2094 |
+
alert("PDF generation failed. Ensure jsPDF is loaded.");
|
| 2095 |
+
}
|
| 2096 |
+
}
|
| 2097 |
+
|
| 2098 |
+
async function saveAttributionMapPNG() {
|
| 2099 |
+
if (!elInputAttributionDisplay) return;
|
| 2100 |
+
try {
|
| 2101 |
+
const canvas = await html2canvas(elInputAttributionDisplay, {
|
| 2102 |
+
backgroundColor: '#ffffff'
|
| 2103 |
+
});
|
| 2104 |
+
|
| 2105 |
+
const link = document.createElement('a');
|
| 2106 |
+
const date = new Date();
|
| 2107 |
+
const timestamp = date.toISOString().replace(/[:.]/g, '-');
|
| 2108 |
+
link.download = `attribution_map_${timestamp}.png`;
|
| 2109 |
+
link.href = canvas.toDataURL('image/png');
|
| 2110 |
+
document.body.appendChild(link);
|
| 2111 |
+
link.click();
|
| 2112 |
+
document.body.removeChild(link);
|
| 2113 |
+
} catch (e) {
|
| 2114 |
+
console.error("PNG save failed", e);
|
| 2115 |
+
alert("Failed to save PNG");
|
| 2116 |
+
}
|
| 2117 |
+
}
|
| 2118 |
+
|
| 2119 |
+
async function saveAttributionMapPDF() {
|
| 2120 |
+
if (!elInputAttributionDisplay) return;
|
| 2121 |
+
try {
|
| 2122 |
+
const { jsPDF } = window.jspdf;
|
| 2123 |
+
// Capture the full DOM node. For long content, we must paginate into multiple PDF pages
|
| 2124 |
+
// instead of trying to create a single ultra-tall page (many viewers/printers will clamp).
|
| 2125 |
+
const canvas = await html2canvas(elInputAttributionDisplay, {
|
| 2126 |
+
backgroundColor: '#ffffff',
|
| 2127 |
+
scale: 2 // Better quality (also increases memory usage)
|
| 2128 |
+
});
|
| 2129 |
+
const imgWidth = canvas.width;
|
| 2130 |
+
const imgHeight = canvas.height;
|
| 2131 |
+
// Create an A4 portrait PDF in px units.
|
| 2132 |
+
// In jsPDF, with unit='px', 1px ~ 1/96 inch.
|
| 2133 |
+
// So A4 is ~ 794 x 1123 px.
|
| 2134 |
+
const doc = new jsPDF({
|
| 2135 |
+
orientation: 'p',
|
| 2136 |
+
unit: 'px',
|
| 2137 |
+
format: 'a4'
|
| 2138 |
+
});
|
| 2139 |
+
const pageWidth = doc.internal.pageSize.getWidth();
|
| 2140 |
+
const pageHeight = doc.internal.pageSize.getHeight();
|
| 2141 |
+
// We scale the capture to fit page width, then slice the source canvas into page-sized chunks.
|
| 2142 |
+
// This is more robust than relying on negative-y offsets (some jsPDF builds/viewers don't clip as expected).
|
| 2143 |
+
const renderWidth = pageWidth;
|
| 2144 |
+
const scale = renderWidth / imgWidth; // PDF px per canvas px
|
| 2145 |
+
// How many source (canvas) pixels fit into one PDF page height after scaling?
|
| 2146 |
+
const pageHeightInCanvasPx = pageHeight / scale;
|
| 2147 |
+
// Basic debug info to help diagnose issues in-browser.
|
| 2148 |
+
const estimatedPages = Math.max(1, Math.ceil(imgHeight / pageHeightInCanvasPx));
|
| 2149 |
+
console.log('[saveAttributionMapPDF] canvas:', { width: imgWidth, height: imgHeight, scale });
|
| 2150 |
+
console.log('[saveAttributionMapPDF] page:', { pageWidth, pageHeight, pageHeightInCanvasPx, estimatedPages });
|
| 2151 |
+
// Re-use a single offscreen canvas for each page slice to save memory.
|
| 2152 |
+
const pageCanvas = document.createElement('canvas');
|
| 2153 |
+
pageCanvas.width = imgWidth;
|
| 2154 |
+
pageCanvas.height = Math.ceil(pageHeightInCanvasPx);
|
| 2155 |
+
const pageCtx = pageCanvas.getContext('2d');
|
| 2156 |
+
let pageIndex = 0;
|
| 2157 |
+
for (let sy = 0; sy < imgHeight; sy += pageHeightInCanvasPx) {
|
| 2158 |
+
const sliceHeight = Math.min(pageHeightInCanvasPx, imgHeight - sy);
|
| 2159 |
+
// Resize pageCanvas height for the last slice to avoid stretching.
|
| 2160 |
+
if (pageCanvas.height !== Math.ceil(sliceHeight)) {
|
| 2161 |
+
pageCanvas.height = Math.ceil(sliceHeight);
|
| 2162 |
+
}
|
| 2163 |
+
// White background
|
| 2164 |
+
pageCtx.fillStyle = '#ffffff';
|
| 2165 |
+
pageCtx.fillRect(0, 0, pageCanvas.width, pageCanvas.height);
|
| 2166 |
+
// Draw slice
|
| 2167 |
+
pageCtx.drawImage(
|
| 2168 |
+
canvas,
|
| 2169 |
+
0,
|
| 2170 |
+
sy,
|
| 2171 |
+
imgWidth,
|
| 2172 |
+
sliceHeight,
|
| 2173 |
+
0,
|
| 2174 |
+
0,
|
| 2175 |
+
imgWidth,
|
| 2176 |
+
sliceHeight
|
| 2177 |
+
);
|
| 2178 |
+
const imgData = pageCanvas.toDataURL('image/jpeg', 0.95);
|
| 2179 |
+
if (pageIndex > 0) doc.addPage();
|
| 2180 |
+
const sliceRenderHeight = sliceHeight * scale;
|
| 2181 |
+
doc.addImage(imgData, 'JPEG', 0, 0, renderWidth, sliceRenderHeight);
|
| 2182 |
+
pageIndex++;
|
| 2183 |
+
}
|
| 2184 |
+
const date = new Date();
|
| 2185 |
+
const timestamp = date.toISOString().replace(/[:.]/g, '-');
|
| 2186 |
+
doc.save(`attribution_map_${timestamp}.pdf`);
|
| 2187 |
+
} catch (e) {
|
| 2188 |
+
console.error("PDF save failed", e);
|
| 2189 |
+
alert("Failed to save PDF. Ensure html2canvas/jspdf loaded.");
|
| 2190 |
+
}
|
| 2191 |
+
}
|
| 2192 |
+
|
| 2193 |
+
// async function saveAttributionMapPDF() {
|
| 2194 |
+
// if (!elInputAttributionDisplay) return;
|
| 2195 |
+
// try {
|
| 2196 |
+
// const { jsPDF } = window.jspdf;
|
| 2197 |
+
// const canvas = await html2canvas(elInputAttributionDisplay, {
|
| 2198 |
+
// backgroundColor: '#ffffff',
|
| 2199 |
+
// scale: 2 // Better quality
|
| 2200 |
+
// });
|
| 2201 |
+
|
| 2202 |
+
// const imgData = canvas.toDataURL('image/jpeg', 1.0);
|
| 2203 |
+
|
| 2204 |
+
// // Calculate PDF dims to match canvas (scaled back to points if needed, or just px)
|
| 2205 |
+
// // Unit 'px' in jsPDF usually corresponds to 1/96 inch, same as canvas pixel?
|
| 2206 |
+
// // Let's match the canvas pixel dimensions exactly for custom format.
|
| 2207 |
+
|
| 2208 |
+
// const imgWidth = canvas.width;
|
| 2209 |
+
// const imgHeight = canvas.height;
|
| 2210 |
+
|
| 2211 |
+
// const orientation = (imgWidth > imgHeight) ? 'l' : 'p';
|
| 2212 |
+
|
| 2213 |
+
// const doc = new jsPDF({
|
| 2214 |
+
// orientation: orientation,
|
| 2215 |
+
// unit: 'px',
|
| 2216 |
+
// format: [imgWidth, imgHeight]
|
| 2217 |
+
// });
|
| 2218 |
+
|
| 2219 |
+
// doc.addImage(imgData, 'JPEG', 0, 0, imgWidth, imgHeight);
|
| 2220 |
+
|
| 2221 |
+
// const date = new Date();
|
| 2222 |
+
// const timestamp = date.toISOString().replace(/[:.]/g, '-');
|
| 2223 |
+
// doc.save(`attribution_map_${timestamp}.pdf`);
|
| 2224 |
+
// } catch (e) {
|
| 2225 |
+
// console.error("PDF save failed", e);
|
| 2226 |
+
// alert("Failed to save PDF. Ensure html2canvas/jspdf loaded.");
|
| 2227 |
+
// }
|
| 2228 |
+
// }
|
| 2229 |
+
|
| 2230 |
+
|
| 2231 |
+
initModelSelectors();
|
frontend/js/main_new.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.104.0
|
| 2 |
+
uvicorn>=0.24.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
torch>=2.0.0
|
| 5 |
+
transformers>=4.40.0
|
| 6 |
+
numpy>=1.24.0
|
| 7 |
+
scipy>=1.10.0
|
| 8 |
+
pandas>=2.0.0
|
| 9 |
+
networkx>=3.0
|
| 10 |
+
huggingface_hub>=0.20.0
|
| 11 |
+
jinja2>=3.1.0
|
| 12 |
+
pyyaml>=6.0
|
| 13 |
+
openai>=1.0.0
|
| 14 |
+
lxt>=0.3.0
|