From 8495e2f33aac1a06021350925999ea42787a5c74 Mon Sep 17 00:00:00 2001 From: WpyQwq <3911625973@qq.com> Date: Sat, 19 Sep 2026 11:59:24 +0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20Neural=20Trace=EF=BC=9A?= =?UTF-8?q?=E8=A7=82=E5=AF=9F=E6=9C=AC=E5=9C=B0=20Qwen3.5-4B=20=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E8=BF=87=E7=A8=8B=E7=9A=84=E6=9E=81=E7=AE=80=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=96=E5=B7=A5=E4=BD=9C=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + README.md | 47 ++ index.html | 13 + package-lock.json | 827 ++++++++++++++++++++++++++++++++++ package.json | 18 + server/__init__.py | 1 + server/main.py | 149 ++++++ server/model_runtime.py | 512 +++++++++++++++++++++ server/requirements-model.txt | 4 + server/requirements.txt | 2 + src/App.jsx | 515 +++++++++++++++++++++ src/Trace3DCanvas.jsx | 150 ++++++ src/main.jsx | 10 + src/styles.css | 311 +++++++++++++ vite.config.js | 12 + 15 files changed, 2576 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/__init__.py create mode 100644 server/main.py create mode 100644 server/model_runtime.py create mode 100644 server/requirements-model.txt create mode 100644 server/requirements.txt create mode 100644 src/App.jsx create mode 100644 src/Trace3DCanvas.jsx create mode 100644 src/main.jsx create mode 100644 src/styles.css create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7caaaa8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.venv/ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f0cf67 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Neural Trace + +一个用于观察本地 Qwen3.5-4B 计算过程的极简可视化工作台。 + +当前版本已完成: + +- 默认 3D trace space:32 层节点、DeltaNet 状态路径、Full Attention 连线、残差流和可点击节点 +- Qwen3.5 混合架构视图:DeltaNet / 线性注意力、Full Attention、Residual Stream、Logits +- Token 流动、激活场、attention map、输出分布和事件时间轴 +- 可播放/暂停/调速/拖动的前向计算模拟器 +- `D:\watch\_LLM\_think` 模型目录健康检查 +- FastAPI SSE 遥测接口,已准备真实 Transformers forward/backward hooks +- Forward、Backward、Attention、Delta State 四种观测模式 +- 可选图片输入:通过 POST SSE 将图像送入 Qwen3.5 Vision Encoder,并显示视觉输入/编码事件 + +## 启动前端 + +```powershell +Set-Location 'E:\neural-dialogue-visualizer' +npm install +npm run dev +``` + +打开 `http://localhost:5173`。 + +## 启动本地遥测服务 + +```powershell +Set-Location 'E:\neural-dialogue-visualizer' +py -3.12 -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r server\requirements.txt +$env:MODEL_DIR = 'D:\watch\_LLM\_think' +python -m uvicorn server.main:app --reload --port 8000 +``` + +模型文件齐全后,如需启用真实 hook runner: + +```powershell +pip install -r server\requirements-model.txt +``` + +真实运行会尝试懒加载本地 Transformers 模型,并在层级模块上注册 forward hook 和 backward hook。当前真实 hooks 路线使用 SafeTensors checkpoint;GGUF 适合 llama.cpp 推理,但不能直接提供这里所需的 PyTorch autograd 层级事件。4B 模型的 backward 需要较大的显存;如果量化 checkpoint 不支持 autograd,服务会保留 mock 流并返回明确的 runtime error。 + +文本 trace 使用 GET `/api/stream?prompt=...&mode=...`;带图片时使用 POST `/api/stream`,请求体为 `{"prompt":"...","mode":"forward","image_data":"data:image/png;base64,..."}`。前端的回形针按钮会自动使用 POST 路径,图片限制为 4 MB。 + +如果模型仍在下载,服务会返回 `mock` 状态,前端继续显示模拟 trace;文件齐全后会显示 `CHECKPOINT READY`。只有 Transformers hook runner 成功加载后才会显示 `MODEL READY`。界面展示的是可观测的激活、状态、注意力和梯度信号,不把隐藏推理文字冒充成“模型思想”。 diff --git a/index.html b/index.html new file mode 100644 index 0000000..713bbd0 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Neural Trace + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1913d0e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,827 @@ +{ + "name": "neural-dialogue-visualizer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "neural-dialogue-visualizer", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "latest", + "lucide-react": "latest", + "react": "latest", + "react-dom": "latest", + "vite": "latest" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.38.0.tgz", + "integrity": "sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5955e12 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "neural-dialogue-visualizer", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@vitejs/plugin-react": "latest", + "lucide-react": "latest", + "react": "latest", + "react-dom": "latest", + "vite": "latest" + } +} diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..b26c46e --- /dev/null +++ b/server/__init__.py @@ -0,0 +1 @@ +"""Neural Trace local telemetry server.""" diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..5e548f4 --- /dev/null +++ b/server/main.py @@ -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"}, + ) diff --git a/server/model_runtime.py b/server/model_runtime.py new file mode 100644 index 0000000..ac0613e --- /dev/null +++ b/server/model_runtime.py @@ -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)} diff --git a/server/requirements-model.txt b/server/requirements-model.txt new file mode 100644 index 0000000..e9e726c --- /dev/null +++ b/server/requirements-model.txt @@ -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 diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..1a15839 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,2 @@ +fastapi>=0.115 +uvicorn[standard]>=0.34 diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..7f42b18 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,515 @@ +import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react' +import { + Activity, + ArrowDown, + BrainCircuit, + ChevronDown, + CircleHelp, + CirclePause, + CirclePlay, + Cpu, + Database, + GitBranch, + Layers3, + MoreHorizontal, + Paperclip, + Pause, + Play, + Radio, + RefreshCw, + Search, + Send, + Settings2, + SlidersHorizontal, + Sparkles, + TerminalSquare, + TimerReset, + Waypoints, + X, +} from 'lucide-react' + +const Trace3DCanvas = lazy(() => import('./Trace3DCanvas.jsx')) + +const TOKENS = ['请', '解', '释', '量', '子', '纠', '缠'] + +const STAGES = [ + { id: 'stage-01', index: '01', title: 'L01—04', type: 'hybrid', caption: '3L + 1A' }, + { id: 'stage-02', index: '02', title: 'L05—08', type: 'hybrid', caption: '3L + 1A' }, + { id: 'stage-03', index: '03', title: 'L09—12', type: 'hybrid', caption: '3L + 1A' }, + { id: 'stage-04', index: '04', title: 'L13—16', type: 'hybrid', caption: '3L + 1A' }, + { id: 'stage-05', index: '05', title: 'L17—20', type: 'hybrid', caption: '3L + 1A' }, + { id: 'stage-06', index: '06', title: 'L21—24', type: 'hybrid', caption: '3L + 1A' }, + { id: 'stage-07', index: '07', title: 'L25—28', type: 'hybrid', caption: '3L + 1A' }, + { id: 'stage-08', index: '08', title: 'L29—32', type: 'hybrid', caption: '3L + 1A' }, +] + +const EVENT_SEED = [ + { time: '00:12.84', text: 'residual stream merged', tone: 'cyan' }, + { time: '00:12.77', text: 'attention pattern stabilized', tone: 'amber' }, + { time: '00:12.68', text: 'token 03 → layer 08', tone: 'cyan' }, + { time: '00:12.51', text: 'delta state updated', tone: 'muted' }, + { time: '00:12.36', text: 'input embedding ready', tone: 'muted' }, +] + +const DEFAULT_ARCHITECTURE = { + num_layers: 32, + pattern: '8 × (3L + 1A)', + hidden_size: 2560, + vocab_size: 248320, + context_length: 262144, + intermediate_size: 9216, + attention_heads: 16, + attention_kv_heads: 4, + attention_head_dim: 256, + vision_encoder: true, + sparse_moe: true, + mtp: true, + precision: 'BF16 / INT4', +} + +const compactNumber = (value) => { + const number = Number(value) + if (!Number.isFinite(number)) return '—' + if (number >= 1000000) return `${(number / 1000000).toFixed(number % 1000000 ? 1 : 0)}M` + if (number >= 100000) return `${Math.round(number / 1000)}K` + return number.toLocaleString('en-US') +} + +function IconButton({ label, children, active = false, onClick }) { + return ( + + ) +} + +function StatusDot({ tone = 'green' }) { + return