Initial commit: Neural Trace:观察本地 Qwen3.5-4B 计算过程的极简可视化工作台
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Neural Trace local telemetry server."""
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
"""Local telemetry service for Neural Trace.
|
||||
|
||||
The service exposes one stable SSE contract for the browser. It uses the
|
||||
actual local checkpoint when the optional model runtime can load it; otherwise
|
||||
it emits clearly labelled mock events so the UI remains usable during download.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncIterator
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .model_runtime import ModelRuntime
|
||||
|
||||
MODEL_DIR = Path(os.getenv("MODEL_DIR", r"D:\watch\_LLM\_think"))
|
||||
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen3.5-4B")
|
||||
runtime = ModelRuntime(MODEL_DIR, MODEL_NAME)
|
||||
|
||||
|
||||
class StreamRequest(BaseModel):
|
||||
prompt: str = "解释一下量子纠缠"
|
||||
mode: str = "forward"
|
||||
image_data: str | None = None
|
||||
|
||||
app = FastAPI(title="Neural Trace Telemetry", version="0.2.0")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:5174",
|
||||
"http://127.0.0.1:5174",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health() -> dict[str, Any]:
|
||||
return {
|
||||
"connected": runtime.ready,
|
||||
"downloaded": runtime.checkpoint_ready,
|
||||
"model": MODEL_NAME,
|
||||
"modelPath": str(runtime.checkpoint_path or MODEL_DIR) if MODEL_DIR.exists() else "waiting for local model",
|
||||
"mode": "model" if runtime.ready else "mock",
|
||||
"runtime": runtime.status,
|
||||
"error": runtime.error,
|
||||
"architecture": runtime.architecture,
|
||||
}
|
||||
|
||||
|
||||
async def mock_trace(prompt: str, mode: str, image_data: str | None = None) -> AsyncIterator[str]:
|
||||
tokens = list(prompt[:10]) or ["·"]
|
||||
stages = ["L01", "L04", "L08", "L12", "L16", "L20", "L24", "L28"]
|
||||
if mode == "backward":
|
||||
stages = list(reversed(stages))
|
||||
if image_data:
|
||||
vision_payload = {
|
||||
"step": 0,
|
||||
"kind": "vision_encoder",
|
||||
"value": 0.72,
|
||||
"shape": [1, vision_payload_size(image_data), 3],
|
||||
"source": "mock",
|
||||
"mode": mode,
|
||||
}
|
||||
yield f"data: {json.dumps(vision_payload, ensure_ascii=False)}\n\n"
|
||||
for step in range(48):
|
||||
if mode == "backward":
|
||||
kind = "gradient"
|
||||
elif mode == "attention":
|
||||
kind = "attention_output"
|
||||
elif mode == "state":
|
||||
kind = "delta_state"
|
||||
elif step % 16 == 11:
|
||||
kind = "mtp_logits"
|
||||
elif step % 12 == 3:
|
||||
kind = "router_weights"
|
||||
elif step % 12 == 7:
|
||||
kind = "expert_mixture"
|
||||
else:
|
||||
kind = "full_attention" if step % 4 == 0 else "delta_state"
|
||||
payload = {
|
||||
"step": step,
|
||||
"token": tokens[step % len(tokens)],
|
||||
"layer": stages[step % len(stages)],
|
||||
"kind": kind,
|
||||
"value": round(0.2 + ((step * 17) % 70) / 100, 3),
|
||||
"source": "mock",
|
||||
"mode": mode,
|
||||
}
|
||||
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||
await asyncio.sleep(0.08)
|
||||
suffix = ",并经过视觉编码分支" if image_data else ""
|
||||
answer = f"演示输出:已完成对“{prompt}”的可观测计算{suffix}。真实模型接入后,这里会替换为本地 Qwen3.5-4B 的生成结果。"
|
||||
yield f"data: {json.dumps({'step': 48, 'kind': 'generation', 'text': answer, 'value': 1.0, 'source': 'mock', 'mode': mode}, ensure_ascii=False)}\n\n"
|
||||
|
||||
|
||||
def vision_payload_size(image_data: str) -> int:
|
||||
return 196
|
||||
|
||||
|
||||
async def runtime_trace(prompt: str, mode: str, image_data: str | None = None) -> AsyncIterator[str]:
|
||||
events = await asyncio.to_thread(runtime.start_trace, prompt, mode, image_data)
|
||||
if events is None:
|
||||
if runtime.error:
|
||||
yield f"data: {json.dumps({'step': 0, 'kind': 'runtime_error', 'error': runtime.error, 'source': 'model_runtime', 'mode': mode}, ensure_ascii=False)}\n\n"
|
||||
async for event in mock_trace(prompt, mode, image_data):
|
||||
yield event
|
||||
return
|
||||
|
||||
while True:
|
||||
payload = await asyncio.to_thread(events.get)
|
||||
if payload.get("kind") == "done":
|
||||
break
|
||||
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||
|
||||
|
||||
@app.get("/api/stream")
|
||||
async def stream(
|
||||
prompt: str = Query(default="解释一下量子纠缠"),
|
||||
mode: str = Query(default="forward"),
|
||||
) -> StreamingResponse:
|
||||
allowed_modes = {"forward", "backward", "attention", "state"}
|
||||
safe_mode = mode if mode in allowed_modes else "forward"
|
||||
return StreamingResponse(
|
||||
runtime_trace(prompt, safe_mode),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/stream")
|
||||
async def stream_post(request: StreamRequest) -> StreamingResponse:
|
||||
safe_mode = request.mode if request.mode in {"forward", "backward", "attention", "state"} else "forward"
|
||||
return StreamingResponse(
|
||||
runtime_trace(request.prompt.strip() or "解释一下量子纠缠", safe_mode, request.image_data),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||
)
|
||||
@@ -0,0 +1,512 @@
|
||||
"""Lazy local model adapter for observable forward and backward traces.
|
||||
|
||||
The adapter is deliberately conservative: it summarizes tensors in the
|
||||
runtime process instead of sending activations or weights to the browser.
|
||||
Large checkpoints are loaded only after the first stream request and failures
|
||||
fall back to the mock event contract in ``main.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import base64
|
||||
import json
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_ARCHITECTURE = {
|
||||
"model_type": "qwen3_5",
|
||||
"num_layers": 32,
|
||||
"pattern": "8 × (3L + 1A)",
|
||||
"hidden_size": 2560,
|
||||
"vocab_size": 248320,
|
||||
"context_length": 262144,
|
||||
"intermediate_size": 9216,
|
||||
"delta_heads_v": 32,
|
||||
"delta_heads_qk": 16,
|
||||
"attention_heads": 16,
|
||||
"attention_kv_heads": 4,
|
||||
"attention_head_dim": 256,
|
||||
"rope_dim": 64,
|
||||
"vision_encoder": True,
|
||||
"sparse_moe": True,
|
||||
"mtp": True,
|
||||
"precision": "BF16 / INT4",
|
||||
}
|
||||
|
||||
|
||||
class ModelRuntime:
|
||||
def __init__(self, model_dir: Path, model_name: str) -> None:
|
||||
self.model_dir = model_dir
|
||||
self.model_name = model_name
|
||||
self.status = "idle"
|
||||
self.error: str | None = None
|
||||
self.model: Any = None
|
||||
self.processor: Any = None
|
||||
self._torch: Any = None
|
||||
self._hooks: list[Any] = []
|
||||
self._load_lock = threading.Lock()
|
||||
self._trace_context = threading.local()
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
return self.status == "ready" and self.model is not None
|
||||
|
||||
@property
|
||||
def checkpoint_path(self) -> Path | None:
|
||||
if not self.model_dir.exists():
|
||||
return None
|
||||
candidates = [self.model_dir]
|
||||
candidates.extend(path for path in sorted(self.model_dir.iterdir()) if path.is_dir())
|
||||
for path in candidates:
|
||||
has_config = (path / "config.json").exists()
|
||||
has_weights = any(path.glob("*.safetensors")) or any(path.glob("*.safetensors.index.json"))
|
||||
if has_config and has_weights:
|
||||
return path
|
||||
return None
|
||||
|
||||
@property
|
||||
def checkpoint_ready(self) -> bool:
|
||||
return self.checkpoint_path is not None
|
||||
|
||||
@property
|
||||
def architecture(self) -> dict[str, Any]:
|
||||
architecture = dict(DEFAULT_ARCHITECTURE)
|
||||
checkpoint_path = self.checkpoint_path
|
||||
if checkpoint_path is None:
|
||||
return architecture
|
||||
try:
|
||||
config = json.loads((checkpoint_path / "config.json").read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError, UnicodeDecodeError):
|
||||
return architecture
|
||||
text_config = config.get("text_config") if isinstance(config.get("text_config"), dict) else config
|
||||
field_map = {
|
||||
"num_hidden_layers": "num_layers",
|
||||
"hidden_size": "hidden_size",
|
||||
"vocab_size": "vocab_size",
|
||||
"max_position_embeddings": "context_length",
|
||||
"intermediate_size": "intermediate_size",
|
||||
"num_attention_heads": "attention_heads",
|
||||
"num_key_value_heads": "attention_kv_heads",
|
||||
"head_dim": "attention_head_dim",
|
||||
"rope_theta_dim": "rope_dim",
|
||||
"model_type": "model_type",
|
||||
}
|
||||
for source_key, target_key in field_map.items():
|
||||
value = text_config.get(source_key)
|
||||
if isinstance(value, (int, float, str)):
|
||||
architecture[target_key] = value
|
||||
if not architecture.get("attention_head_dim") and architecture.get("attention_heads"):
|
||||
architecture["attention_head_dim"] = architecture["hidden_size"] // architecture["attention_heads"]
|
||||
return architecture
|
||||
|
||||
def ensure_loaded(self) -> bool:
|
||||
if self.ready:
|
||||
return True
|
||||
checkpoint_path = self.checkpoint_path
|
||||
if checkpoint_path is None:
|
||||
self.status = "downloading"
|
||||
return False
|
||||
|
||||
with self._load_lock:
|
||||
if self.ready:
|
||||
return True
|
||||
self.status = "loading"
|
||||
self.error = None
|
||||
try:
|
||||
import torch
|
||||
from transformers import AutoModelForCausalLM, AutoProcessor
|
||||
|
||||
self._torch = torch
|
||||
try:
|
||||
self.processor = AutoProcessor.from_pretrained(checkpoint_path, local_files_only=True, trust_remote_code=True)
|
||||
except Exception:
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
self.processor = AutoTokenizer.from_pretrained(checkpoint_path, local_files_only=True, trust_remote_code=True)
|
||||
|
||||
model_class = AutoModelForCausalLM
|
||||
image_text_model = getattr(__import__("transformers", fromlist=["AutoModelForImageTextToText"]), "AutoModelForImageTextToText", None)
|
||||
if image_text_model is not None:
|
||||
model_class = image_text_model
|
||||
|
||||
load_kwargs = {
|
||||
"local_files_only": True,
|
||||
"trust_remote_code": True,
|
||||
"torch_dtype": "auto",
|
||||
}
|
||||
if importlib.util.find_spec("accelerate") is not None:
|
||||
load_kwargs.update(device_map="auto", low_cpu_mem_usage=True)
|
||||
|
||||
try:
|
||||
self.model = model_class.from_pretrained(
|
||||
checkpoint_path,
|
||||
**load_kwargs,
|
||||
)
|
||||
except Exception:
|
||||
self.model = AutoModelForCausalLM.from_pretrained(
|
||||
checkpoint_path,
|
||||
**load_kwargs,
|
||||
)
|
||||
|
||||
self.model.eval()
|
||||
self._install_hooks()
|
||||
self.status = "ready"
|
||||
return True
|
||||
except Exception as exc: # pragma: no cover - depends on local GPU/runtime
|
||||
self.status = "error"
|
||||
self.error = f"{type(exc).__name__}: {exc}"
|
||||
self.model = None
|
||||
return False
|
||||
|
||||
def start_trace(self, prompt: str, mode: str, image_data: str | None = None) -> queue.Queue[dict[str, Any]] | None:
|
||||
if not self.ensure_loaded():
|
||||
return None
|
||||
events: queue.Queue[dict[str, Any]] = queue.Queue()
|
||||
worker = threading.Thread(target=self._trace_worker, args=(prompt, mode, image_data, events), daemon=True)
|
||||
worker.start()
|
||||
return events
|
||||
|
||||
def _install_hooks(self) -> None:
|
||||
if self._hooks or self.model is None:
|
||||
return
|
||||
|
||||
seen_layers: set[int] = set()
|
||||
for name, module in self.model.named_modules():
|
||||
match = re.search(r"(?:^|\.)(?:language_model\.)?(?:layers|(?:model|transformer|decoder)\.(?:layers|h))\.(\d+)$", name)
|
||||
if match is not None:
|
||||
layer_index = int(match.group(1))
|
||||
if layer_index not in seen_layers:
|
||||
seen_layers.add(layer_index)
|
||||
self._hooks.append(module.register_forward_hook(self._make_forward_hook(layer_index)))
|
||||
self._hooks.append(module.register_full_backward_hook(self._make_backward_hook(layer_index)))
|
||||
elif re.search(r"(?:^|\.)\b(?:visual|vision_model|vision_tower)\b$", name):
|
||||
self._hooks.append(module.register_forward_hook(self._make_vision_hook()))
|
||||
elif re.search(r"(?:^|\.)\b(?:router|routing|gate|experts|expert_gate|mtp)\b$", name, re.IGNORECASE):
|
||||
kind = "router_weights" if re.search(r"router|routing|gate", name, re.IGNORECASE) else "expert_mixture"
|
||||
if re.search(r"mtp", name, re.IGNORECASE):
|
||||
kind = "mtp_logits"
|
||||
self._hooks.append(module.register_forward_hook(self._make_aux_hook(name, kind)))
|
||||
elif re.search(r"(?:^|\.)lm_head$", name):
|
||||
self._hooks.append(module.register_forward_hook(self._make_logits_hook()))
|
||||
|
||||
def _make_forward_hook(self, layer_index: int):
|
||||
def forward_hook(_module: Any, _inputs: Any, output: Any) -> None:
|
||||
context = getattr(self._trace_context, "queue", None)
|
||||
if context is None:
|
||||
return
|
||||
tensor = self._first_tensor(output)
|
||||
summary = self._summarize(tensor)
|
||||
full_attention = (layer_index + 1) % 4 == 0
|
||||
mode = getattr(self._trace_context, "mode", "forward")
|
||||
step = getattr(self._trace_context, "step", 0)
|
||||
self._trace_context.step = step + 1
|
||||
if mode == "attention":
|
||||
kind = "attention_output" if full_attention else "delta_context"
|
||||
elif mode == "state":
|
||||
kind = "delta_state"
|
||||
else:
|
||||
kind = "full_attention" if full_attention else "delta_state"
|
||||
context.put({
|
||||
"step": min(step, 511),
|
||||
"layer": f"L{layer_index + 1:02d}",
|
||||
"kind": kind,
|
||||
"mode": mode,
|
||||
"value": summary["rms"],
|
||||
"mean": summary["mean"],
|
||||
"shape": summary["shape"],
|
||||
"source": "forward_hook",
|
||||
})
|
||||
|
||||
return forward_hook
|
||||
|
||||
def _make_vision_hook(self):
|
||||
def vision_hook(_module: Any, _inputs: Any, output: Any) -> None:
|
||||
context = getattr(self._trace_context, "queue", None)
|
||||
if context is None:
|
||||
return
|
||||
tensor = self._first_tensor(output)
|
||||
summary = self._summarize(tensor)
|
||||
step = getattr(self._trace_context, "step", 0)
|
||||
self._trace_context.step = step + 1
|
||||
context.put({
|
||||
"step": min(step, 511),
|
||||
"kind": "vision_encoder",
|
||||
"mode": getattr(self._trace_context, "mode", "forward"),
|
||||
"value": summary["rms"],
|
||||
"mean": summary["mean"],
|
||||
"shape": summary["shape"],
|
||||
"source": "vision_hook",
|
||||
})
|
||||
|
||||
return vision_hook
|
||||
|
||||
def _make_aux_hook(self, module_name: str, kind: str):
|
||||
def aux_hook(_module: Any, _inputs: Any, output: Any) -> None:
|
||||
context = getattr(self._trace_context, "queue", None)
|
||||
if context is None:
|
||||
return
|
||||
tensor = self._first_tensor(output)
|
||||
summary = self._summarize(tensor)
|
||||
layer_match = re.search(r"layers\.(\d+)", module_name)
|
||||
layer = f"L{int(layer_match.group(1)) + 1:02d}" if layer_match else "AUX"
|
||||
step = getattr(self._trace_context, "step", 0)
|
||||
self._trace_context.step = step + 1
|
||||
context.put({
|
||||
"step": min(step, 511),
|
||||
"layer": layer,
|
||||
"kind": kind,
|
||||
"mode": getattr(self._trace_context, "mode", "forward"),
|
||||
"value": summary["rms"],
|
||||
"mean": summary["mean"],
|
||||
"shape": summary["shape"],
|
||||
"module": module_name,
|
||||
"source": "aux_hook",
|
||||
})
|
||||
|
||||
return aux_hook
|
||||
|
||||
def _make_backward_hook(self, layer_index: int):
|
||||
def backward_hook(_module: Any, _grad_input: Any, grad_output: Any) -> None:
|
||||
context = getattr(self._trace_context, "queue", None)
|
||||
if context is None or getattr(self._trace_context, "mode", "forward") != "backward":
|
||||
return
|
||||
tensor = self._first_tensor(grad_output)
|
||||
summary = self._summarize(tensor)
|
||||
step = getattr(self._trace_context, "step", 0)
|
||||
self._trace_context.step = step + 1
|
||||
context.put({
|
||||
"step": min(step, 511),
|
||||
"layer": f"L{layer_index + 1:02d}",
|
||||
"kind": "gradient",
|
||||
"value": summary["rms"],
|
||||
"mean": summary["mean"],
|
||||
"shape": summary["shape"],
|
||||
"source": "backward_hook",
|
||||
})
|
||||
|
||||
return backward_hook
|
||||
|
||||
def _make_logits_hook(self):
|
||||
def logits_hook(_module: Any, _inputs: Any, output: Any) -> None:
|
||||
context = getattr(self._trace_context, "queue", None)
|
||||
if context is None:
|
||||
return
|
||||
tensor = self._first_tensor(output)
|
||||
if tensor is not None and tensor.ndim >= 2:
|
||||
tensor = tensor[:, -1, :]
|
||||
summary = self._summarize(tensor)
|
||||
step = getattr(self._trace_context, "step", 0)
|
||||
self._trace_context.step = step + 1
|
||||
context.put({
|
||||
"step": min(step, 511),
|
||||
"layer": "OUT",
|
||||
"kind": "logits",
|
||||
"value": summary["rms"],
|
||||
"mean": summary["mean"],
|
||||
"shape": summary["shape"],
|
||||
"source": "logits_hook",
|
||||
})
|
||||
|
||||
return logits_hook
|
||||
|
||||
def _trace_worker(self, prompt: str, mode: str, image_data: str | None, events: queue.Queue[dict[str, Any]]) -> None:
|
||||
self._trace_context.queue = events
|
||||
self._trace_context.mode = mode
|
||||
self._trace_context.step = 0
|
||||
try:
|
||||
inputs = self._encode(prompt, image_data)
|
||||
if image_data and "pixel_values" in inputs:
|
||||
summary = self._summarize(inputs["pixel_values"])
|
||||
events.put({
|
||||
"step": 0,
|
||||
"kind": "vision_input",
|
||||
"mode": mode,
|
||||
"value": summary["rms"],
|
||||
"mean": summary["mean"],
|
||||
"shape": summary["shape"],
|
||||
"source": "processor",
|
||||
})
|
||||
if mode == "backward":
|
||||
self._run_backward(inputs, events)
|
||||
elif mode == "attention":
|
||||
self._run_attention(inputs, events)
|
||||
else:
|
||||
self._run_generation(inputs, events)
|
||||
except Exception as exc: # pragma: no cover - depends on checkpoint/runtime
|
||||
events.put({"kind": "runtime_error", "error": f"{type(exc).__name__}: {exc}", "source": "model_runtime"})
|
||||
finally:
|
||||
events.put({"kind": "done", "source": "model_runtime"})
|
||||
self._trace_context.queue = None
|
||||
|
||||
def _run_attention(self, inputs: dict[str, Any], events: queue.Queue[dict[str, Any]]) -> None:
|
||||
torch = self._torch
|
||||
try:
|
||||
with torch.inference_mode():
|
||||
outputs = self.model(
|
||||
**inputs,
|
||||
use_cache=False,
|
||||
output_attentions=True,
|
||||
return_dict=True,
|
||||
)
|
||||
attentions = getattr(outputs, "attentions", None)
|
||||
emitted = False
|
||||
if attentions:
|
||||
for layer_index, attention in enumerate(attentions):
|
||||
if attention is None or getattr(attention, "ndim", 0) < 4:
|
||||
continue
|
||||
size = min(7, int(attention.shape[-1]))
|
||||
matrix = attention[0, :, -size:, -size:].float().mean(dim=0)
|
||||
values = matrix.detach().cpu().reshape(-1).tolist()
|
||||
summary = self._summarize(matrix)
|
||||
step = getattr(self._trace_context, "step", 0)
|
||||
self._trace_context.step = step + 1
|
||||
events.put({
|
||||
"step": min(step, 511),
|
||||
"layer": f"L{layer_index + 1:02d}",
|
||||
"kind": "attention_weight",
|
||||
"mode": "attention",
|
||||
"value": summary["rms"],
|
||||
"mean": summary["mean"],
|
||||
"shape": list(attention.shape),
|
||||
"attention": [round(float(value), 6) for value in values],
|
||||
"attention_shape": [size, size],
|
||||
"source": "attention_output",
|
||||
})
|
||||
emitted = True
|
||||
if not emitted:
|
||||
events.put({
|
||||
"step": min(getattr(self._trace_context, "step", 0), 511),
|
||||
"kind": "attention_unavailable",
|
||||
"mode": "attention",
|
||||
"source": "model_runtime",
|
||||
})
|
||||
except Exception:
|
||||
events.put({
|
||||
"step": min(getattr(self._trace_context, "step", 0), 511),
|
||||
"kind": "attention_unavailable",
|
||||
"mode": "attention",
|
||||
"source": "model_runtime",
|
||||
})
|
||||
self._run_generation(inputs, events)
|
||||
|
||||
def _encode(self, prompt: str, image_data: str | None = None) -> dict[str, Any]:
|
||||
processor = self.processor
|
||||
rendered_prompt = prompt
|
||||
image = None
|
||||
if image_data:
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
encoded_image = image_data.split(",", 1)[-1]
|
||||
image = Image.open(BytesIO(base64.b64decode(encoded_image))).convert("RGB")
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"invalid image input: {type(exc).__name__}") from exc
|
||||
apply_chat_template = getattr(processor, "apply_chat_template", None)
|
||||
if callable(apply_chat_template):
|
||||
try:
|
||||
content = [{"type": "image", "image": image}, {"type": "text", "text": prompt}] if image is not None else prompt
|
||||
rendered_prompt = apply_chat_template(
|
||||
[{"role": "user", "content": content}],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
except Exception:
|
||||
rendered_prompt = prompt
|
||||
processor_kwargs: dict[str, Any] = {"text": rendered_prompt, "return_tensors": "pt", "truncation": True, "max_length": 256}
|
||||
if image is not None:
|
||||
processor_kwargs["images"] = image
|
||||
try:
|
||||
encoded = processor(**processor_kwargs)
|
||||
except Exception:
|
||||
processor_kwargs.pop("truncation", None)
|
||||
processor_kwargs.pop("max_length", None)
|
||||
encoded = processor(**processor_kwargs)
|
||||
device = self._input_device()
|
||||
return {key: value.to(device) if hasattr(value, "to") else value for key, value in encoded.items()}
|
||||
|
||||
def _run_generation(self, inputs: dict[str, Any], events: queue.Queue[dict[str, Any]]) -> None:
|
||||
torch = self._torch
|
||||
with torch.inference_mode():
|
||||
generated = self.model.generate(**inputs, max_new_tokens=32, do_sample=False, use_cache=True)
|
||||
if hasattr(generated, "sequences"):
|
||||
generated = generated.sequences
|
||||
input_length = 0
|
||||
if "input_ids" in inputs and getattr(inputs["input_ids"], "ndim", 0) >= 2:
|
||||
input_length = int(inputs["input_ids"].shape[-1])
|
||||
if getattr(generated, "ndim", 0) >= 2 and input_length:
|
||||
generated = generated[:, input_length:]
|
||||
try:
|
||||
text = self.processor.batch_decode(generated, skip_special_tokens=True)[0]
|
||||
except Exception:
|
||||
tokenizer = getattr(self.processor, "tokenizer", None)
|
||||
try:
|
||||
text = tokenizer.batch_decode(generated, skip_special_tokens=True)[0] if tokenizer is not None else ""
|
||||
except Exception:
|
||||
text = ""
|
||||
events.put({
|
||||
"step": min(getattr(self._trace_context, "step", 0), 511),
|
||||
"kind": "generation",
|
||||
"token": text[-1:] if text else "",
|
||||
"text": text,
|
||||
"value": 1.0,
|
||||
"source": "model_runtime",
|
||||
})
|
||||
|
||||
def _run_backward(self, inputs: dict[str, Any], events: queue.Queue[dict[str, Any]]) -> None:
|
||||
torch = self._torch
|
||||
if "input_ids" not in inputs:
|
||||
raise RuntimeError("processor did not return input_ids; backward trace requires text tokens")
|
||||
self.model.zero_grad(set_to_none=True)
|
||||
labels = inputs["input_ids"].clone()
|
||||
with torch.enable_grad():
|
||||
outputs = self.model(**inputs, labels=labels, use_cache=False)
|
||||
loss = outputs.loss
|
||||
loss.backward()
|
||||
events.put({
|
||||
"step": min(getattr(self._trace_context, "step", 0), 511),
|
||||
"kind": "loss",
|
||||
"loss": float(loss.detach().float().item()),
|
||||
"value": float(loss.detach().float().item()),
|
||||
"source": "autograd",
|
||||
})
|
||||
|
||||
def _input_device(self):
|
||||
try:
|
||||
return self.model.device
|
||||
except Exception:
|
||||
for parameter in self.model.parameters():
|
||||
if parameter.device.type != "meta":
|
||||
return parameter.device
|
||||
return self._torch.device("cpu")
|
||||
|
||||
def _first_tensor(self, value: Any):
|
||||
if self._torch is not None and self._torch.is_tensor(value):
|
||||
return value
|
||||
if isinstance(value, (tuple, list)):
|
||||
for item in value:
|
||||
tensor = self._first_tensor(item)
|
||||
if tensor is not None:
|
||||
return tensor
|
||||
if isinstance(value, dict):
|
||||
for item in value.values():
|
||||
tensor = self._first_tensor(item)
|
||||
if tensor is not None:
|
||||
return tensor
|
||||
return None
|
||||
|
||||
def _summarize(self, tensor: Any) -> dict[str, Any]:
|
||||
if tensor is None:
|
||||
return {"mean": 0.0, "rms": 0.0, "shape": []}
|
||||
torch = self._torch
|
||||
sampled = tensor.detach().float().reshape(-1)
|
||||
if sampled.numel() > 4096:
|
||||
stride = max(1, sampled.numel() // 4096)
|
||||
sampled = sampled[::stride]
|
||||
mean = float(sampled.mean().item())
|
||||
rms = float(torch.sqrt(torch.mean(sampled * sampled)).item())
|
||||
return {"mean": round(mean, 6), "rms": round(rms, 6), "shape": list(tensor.shape)}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Install this optional set after the Qwen3.5 checkpoint is fully downloaded.
|
||||
torch>=2.6
|
||||
transformers>=4.57
|
||||
accelerate>=1.3
|
||||
@@ -0,0 +1,2 @@
|
||||
fastapi>=0.115
|
||||
uvicorn[standard]>=0.34
|
||||
Reference in New Issue
Block a user