@simulatte/doppler 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +16 -23
  3. package/package.json +30 -32
  4. package/src/adapters/adapter-registry.js +12 -1
  5. package/src/adapters/lora-loader.js +23 -6
  6. package/src/bridge/extension-client.d.ts +5 -0
  7. package/src/bridge/extension-client.js +40 -0
  8. package/src/bridge/index.d.ts +2 -1
  9. package/src/bridge/index.js +6 -4
  10. package/src/browser/browser-converter.js +31 -1
  11. package/src/browser/file-picker.js +6 -0
  12. package/src/browser/safetensors-parser-browser.js +84 -1
  13. package/src/browser/shard-io-browser.js +2 -2
  14. package/src/browser/tensor-source-download.js +8 -2
  15. package/src/browser/tensor-source-http.d.ts +1 -0
  16. package/src/browser/tensor-source-http.js +5 -1
  17. package/src/client/doppler-api.browser.js +20 -4
  18. package/src/client/doppler-api.js +19 -3
  19. package/src/client/doppler-provider/generation.js +12 -0
  20. package/src/client/doppler-provider/model-manager.d.ts +10 -0
  21. package/src/client/doppler-provider/model-manager.js +91 -19
  22. package/src/client/doppler-provider/source-runtime.d.ts +2 -1
  23. package/src/client/doppler-provider/source-runtime.js +132 -13
  24. package/src/client/doppler-registry.json +5 -20
  25. package/src/config/backward-registry-loader.js +17 -2
  26. package/src/config/execution-v0-contract-check.js +113 -15
  27. package/src/config/kernel-path-contract-check.js +57 -29
  28. package/src/config/kernel-path-loader.d.ts +5 -0
  29. package/src/config/kernel-path-loader.js +18 -36
  30. package/src/config/kernels/kernel-ref-digests.js +1 -1
  31. package/src/config/kernels/registry.js +14 -1
  32. package/src/config/kernels/registry.json +81 -5
  33. package/src/config/loader.d.ts +1 -1
  34. package/src/config/loader.js +15 -2
  35. package/src/config/merge-contract-check.js +66 -4
  36. package/src/config/merge-helpers.js +128 -7
  37. package/src/config/merge.d.ts +1 -0
  38. package/src/config/merge.js +10 -0
  39. package/src/config/param-validator.js +47 -2
  40. package/src/config/presets/kernel-paths/{gemma2-q4k-dequant-f32a.json → gemma2-q4k-dequant-f32a-nosubgroups.json} +3 -3
  41. package/src/config/presets/kernel-paths/gemma3-f16-fused-f32a-online-streamingprefill.json +223 -0
  42. package/src/config/presets/kernel-paths/{gemma3-q4k-dequant-f32a.json → gemma3-q4k-dequant-f32a-nosubgroups.json} +3 -3
  43. package/src/config/presets/kernel-paths/gemma3-q4k-dequant-f32w-f32a-online.json +56 -0
  44. package/src/config/presets/kernel-paths/lfm2-q4k-dequant-f32a-nosubgroups.json +61 -0
  45. package/src/config/presets/kernel-paths/registry.json +43 -8
  46. package/src/config/presets/models/gemma2.json +3 -2
  47. package/src/config/presets/models/gemma3.json +2 -0
  48. package/src/config/presets/models/qwen3.json +4 -3
  49. package/src/config/presets/models/qwen3_5.json +16 -0
  50. package/src/config/presets/runtime/experiments/bench/gemma3-bench-q4k.json +1 -1
  51. package/src/config/presets/runtime/experiments/debug/gemma3-debug-q4k.json +1 -1
  52. package/src/config/presets/runtime/experiments/verify/gemma3-verify.json +1 -1
  53. package/src/config/presets/runtime/kernels/dequant-f16-q4k.json +6 -13
  54. package/src/config/presets/runtime/kernels/dequant-f32-q4k.json +6 -13
  55. package/src/config/presets/runtime/kernels/embeddinggemma-q4k-dequant-f32a.json +37 -0
  56. package/src/config/presets/runtime/kernels/fused-q4k.json +6 -13
  57. package/src/config/presets/runtime/kernels/gemma2-q4k-dequant-f16a.json +33 -0
  58. package/src/config/presets/runtime/kernels/gemma2-q4k-dequant-f32a-nosubgroups.json +33 -0
  59. package/src/config/presets/runtime/kernels/gemma2-q4k-fused-f32a.json +33 -0
  60. package/src/config/presets/runtime/kernels/safe-q4k.json +6 -13
  61. package/src/config/presets/runtime/model/qwen3-5-layer-probe.json +52 -0
  62. package/src/config/presets/runtime/model/qwen3-5-linear-attn-debug.json +90 -0
  63. package/src/config/presets/runtime/platform/metal-apple-q4k.json +1 -1
  64. package/src/config/runtime.js +6 -1
  65. package/src/config/schema/conversion.schema.d.ts +1 -0
  66. package/src/config/schema/debug.schema.d.ts +5 -0
  67. package/src/config/schema/doppler.schema.js +16 -21
  68. package/src/config/schema/inference-defaults.schema.js +3 -3
  69. package/src/config/schema/kernel-path.schema.d.ts +5 -1
  70. package/src/config/schema/kernel-thresholds.schema.js +12 -4
  71. package/src/config/schema/manifest.schema.d.ts +3 -2
  72. package/src/config/schema/manifest.schema.js +17 -4
  73. package/src/config/schema/storage.schema.js +1 -1
  74. package/src/config/training-defaults.js +30 -22
  75. package/src/converter/conversion-plan.js +104 -11
  76. package/src/converter/core.d.ts +7 -0
  77. package/src/converter/core.js +16 -9
  78. package/src/converter/execution-v0-manifest.js +4 -1
  79. package/src/converter/index.d.ts +1 -0
  80. package/src/converter/index.js +1 -0
  81. package/src/converter/manifest-inference.js +50 -29
  82. package/src/converter/parsers/diffusion.js +0 -3
  83. package/src/converter/parsers/transformer.js +4 -0
  84. package/src/converter/quantization-info.js +40 -16
  85. package/src/converter/quantizer.js +19 -12
  86. package/src/converter/rope-config.js +8 -6
  87. package/src/converter/shard-packer.d.ts +1 -1
  88. package/src/converter/shard-packer.js +4 -1
  89. package/src/converter/tokenizer-utils.d.ts +1 -0
  90. package/src/converter/tokenizer-utils.js +4 -1
  91. package/src/debug/config.js +123 -11
  92. package/src/debug/reference/hf_qwen35_linear_attn_debug.py +268 -0
  93. package/src/debug/signals.js +7 -1
  94. package/src/debug/tensor.d.ts +2 -0
  95. package/src/debug/tensor.js +13 -2
  96. package/src/distribution/p2p-control-plane.js +52 -12
  97. package/src/distribution/p2p-observability.js +43 -7
  98. package/src/distribution/p2p-webrtc-browser.js +20 -0
  99. package/src/distribution/shard-delivery.js +83 -27
  100. package/src/formats/gguf/types.js +33 -16
  101. package/src/formats/rdrr/groups.d.ts +12 -4
  102. package/src/formats/rdrr/groups.js +3 -6
  103. package/src/formats/rdrr/parsing.d.ts +4 -0
  104. package/src/formats/rdrr/parsing.js +53 -3
  105. package/src/formats/rdrr/types.d.ts +2 -1
  106. package/src/gpu/command-recorder.js +86 -61
  107. package/src/gpu/device.d.ts +1 -0
  108. package/src/gpu/device.js +73 -19
  109. package/src/gpu/kernel-tuner/benchmarks.js +326 -316
  110. package/src/gpu/kernel-tuner/cache.js +71 -4
  111. package/src/gpu/kernel-tuner/tuner.js +22 -4
  112. package/src/gpu/kernels/attention.js +15 -34
  113. package/src/gpu/kernels/backward/adam.js +62 -58
  114. package/src/gpu/kernels/backward/attention_backward.js +257 -169
  115. package/src/gpu/kernels/backward/conv2d_backward.js +14 -1
  116. package/src/gpu/kernels/cast.js +191 -149
  117. package/src/gpu/kernels/check-stop.js +33 -44
  118. package/src/gpu/kernels/conv2d.js +27 -17
  119. package/src/gpu/kernels/cross_entropy_loss.js +21 -15
  120. package/src/gpu/kernels/depthwise_conv2d.js +36 -26
  121. package/src/gpu/kernels/dequant.js +178 -126
  122. package/src/gpu/kernels/energy.d.ts +3 -21
  123. package/src/gpu/kernels/energy.js +111 -88
  124. package/src/gpu/kernels/feature-check.js +1 -1
  125. package/src/gpu/kernels/fused_ffn.js +84 -65
  126. package/src/gpu/kernels/fused_matmul_residual.js +56 -33
  127. package/src/gpu/kernels/fused_matmul_rmsnorm.js +62 -45
  128. package/src/gpu/kernels/gather.js +33 -15
  129. package/src/gpu/kernels/gelu.js +19 -11
  130. package/src/gpu/kernels/grouped_pointwise_conv2d.js +33 -23
  131. package/src/gpu/kernels/groupnorm.js +34 -23
  132. package/src/gpu/kernels/index.d.ts +8 -0
  133. package/src/gpu/kernels/index.js +6 -0
  134. package/src/gpu/kernels/kv-quantize.js +5 -2
  135. package/src/gpu/kernels/layernorm.js +35 -19
  136. package/src/gpu/kernels/logit-merge.js +5 -3
  137. package/src/gpu/kernels/matmul-selection.js +47 -4
  138. package/src/gpu/kernels/matmul.d.ts +2 -0
  139. package/src/gpu/kernels/matmul.js +59 -40
  140. package/src/gpu/kernels/modulate.js +23 -15
  141. package/src/gpu/kernels/moe.js +221 -175
  142. package/src/gpu/kernels/pixel_shuffle.js +22 -14
  143. package/src/gpu/kernels/relu.js +18 -10
  144. package/src/gpu/kernels/repeat_channels.js +25 -17
  145. package/src/gpu/kernels/residual.js +37 -27
  146. package/src/gpu/kernels/rmsnorm.js +66 -43
  147. package/src/gpu/kernels/rope.js +3 -0
  148. package/src/gpu/kernels/sample.js +27 -38
  149. package/src/gpu/kernels/sana_linear_attention.js +18 -10
  150. package/src/gpu/kernels/scale.js +18 -11
  151. package/src/gpu/kernels/shader-cache.js +4 -2
  152. package/src/gpu/kernels/silu.js +120 -72
  153. package/src/gpu/kernels/softmax.js +44 -25
  154. package/src/gpu/kernels/split_qg.d.ts +50 -0
  155. package/src/gpu/kernels/split_qg.js +46 -0
  156. package/src/gpu/kernels/split_qg.wgsl +58 -0
  157. package/src/gpu/kernels/split_qg_f16.wgsl +62 -0
  158. package/src/gpu/kernels/split_qkv.js +23 -13
  159. package/src/gpu/kernels/transpose.js +18 -10
  160. package/src/gpu/kernels/transpose.wgsl +5 -3
  161. package/src/gpu/kernels/upsample2d.js +21 -13
  162. package/src/gpu/kernels/utils.js +20 -13
  163. package/src/gpu/partitioned-buffer-pool.js +10 -2
  164. package/src/gpu/perf-guards.js +2 -9
  165. package/src/gpu/profiler.js +27 -22
  166. package/src/gpu/readback-utils.d.ts +16 -0
  167. package/src/gpu/readback-utils.js +41 -0
  168. package/src/gpu/submit-tracker.js +13 -0
  169. package/src/gpu/uniform-cache.d.ts +1 -0
  170. package/src/gpu/uniform-cache.js +30 -9
  171. package/src/gpu/weight-buffer.d.ts +1 -1
  172. package/src/gpu/weight-buffer.js +1 -1
  173. package/src/hotswap/intent-bundle.js +6 -0
  174. package/src/hotswap/manifest.d.ts +10 -1
  175. package/src/hotswap/manifest.js +12 -2
  176. package/src/hotswap/runtime.js +30 -8
  177. package/src/index-browser.d.ts +44 -0
  178. package/src/index-browser.js +14 -0
  179. package/src/inference/browser-harness-contract-helpers.d.ts +5 -0
  180. package/src/inference/browser-harness-contract-helpers.js +28 -0
  181. package/src/inference/browser-harness-diffusion-energy-suites.d.ts +2 -0
  182. package/src/inference/browser-harness-diffusion-energy-suites.js +269 -0
  183. package/src/inference/browser-harness-model-helpers.d.ts +16 -0
  184. package/src/inference/browser-harness-model-helpers.js +217 -0
  185. package/src/inference/browser-harness-report-helpers.d.ts +7 -0
  186. package/src/inference/browser-harness-report-helpers.js +42 -0
  187. package/src/inference/browser-harness-runtime-helpers.d.ts +61 -0
  188. package/src/inference/browser-harness-runtime-helpers.js +415 -0
  189. package/src/inference/browser-harness-suite-helpers.d.ts +28 -0
  190. package/src/inference/browser-harness-suite-helpers.js +268 -0
  191. package/src/inference/browser-harness-text-helpers.d.ts +27 -0
  192. package/src/inference/browser-harness-text-helpers.js +788 -0
  193. package/src/inference/browser-harness.d.ts +8 -0
  194. package/src/inference/browser-harness.js +149 -1996
  195. package/src/inference/kv-cache/base.js +140 -94
  196. package/src/inference/kv-cache/tiered.js +5 -3
  197. package/src/inference/moe-router.js +88 -56
  198. package/src/inference/multi-model-network.js +5 -3
  199. package/src/inference/network-evolution.d.ts +11 -2
  200. package/src/inference/network-evolution.js +20 -21
  201. package/src/inference/pipelines/context.d.ts +3 -0
  202. package/src/inference/pipelines/context.js +142 -2
  203. package/src/inference/pipelines/diffusion/helpers.js +10 -2
  204. package/src/inference/pipelines/diffusion/pipeline.js +2 -1
  205. package/src/inference/pipelines/diffusion/sd3-transformer.js +10 -10
  206. package/src/inference/pipelines/diffusion/text-encoder-gpu.js +8 -2
  207. package/src/inference/pipelines/diffusion/vae.js +3 -7
  208. package/src/inference/pipelines/energy/pipeline.js +27 -21
  209. package/src/inference/pipelines/energy/quintel.d.ts +5 -0
  210. package/src/inference/pipelines/energy/quintel.js +11 -0
  211. package/src/inference/pipelines/energy-head/row-head-pipeline.js +17 -13
  212. package/src/inference/pipelines/structured/json-head-pipeline.js +26 -11
  213. package/src/inference/pipelines/text/attention/output-projection.d.ts +12 -0
  214. package/src/inference/pipelines/text/attention/output-projection.js +8 -0
  215. package/src/inference/pipelines/text/attention/projections.d.ts +10 -1
  216. package/src/inference/pipelines/text/attention/projections.js +192 -112
  217. package/src/inference/pipelines/text/attention/record.js +77 -14
  218. package/src/inference/pipelines/text/attention/run.js +112 -14
  219. package/src/inference/pipelines/text/config.js +17 -4
  220. package/src/inference/pipelines/text/embed.js +2 -8
  221. package/src/inference/pipelines/text/execution-plan.js +46 -23
  222. package/src/inference/pipelines/text/execution-v0-contract-helpers.d.ts +59 -0
  223. package/src/inference/pipelines/text/execution-v0-contract-helpers.js +937 -0
  224. package/src/inference/pipelines/text/execution-v0-runtime-builders.d.ts +15 -0
  225. package/src/inference/pipelines/text/execution-v0-runtime-builders.js +279 -0
  226. package/src/inference/pipelines/text/execution-v0.js +62 -1013
  227. package/src/inference/pipelines/text/generator-runtime.js +5 -0
  228. package/src/inference/pipelines/text/generator-steps.d.ts +52 -0
  229. package/src/inference/pipelines/text/generator-steps.js +340 -221
  230. package/src/inference/pipelines/text/generator.js +56 -40
  231. package/src/inference/pipelines/text/init.d.ts +13 -0
  232. package/src/inference/pipelines/text/init.js +94 -25
  233. package/src/inference/pipelines/text/kernel-path-auto-select.js +2 -0
  234. package/src/inference/pipelines/text/kernel-trace.d.ts +2 -0
  235. package/src/inference/pipelines/text/kernel-trace.js +6 -0
  236. package/src/inference/pipelines/text/layer.js +4 -9
  237. package/src/inference/pipelines/text/linear-attention.d.ts +15 -0
  238. package/src/inference/pipelines/text/linear-attention.js +113 -9
  239. package/src/inference/pipelines/text/logits/gpu.js +12 -7
  240. package/src/inference/pipelines/text/logits/index.d.ts +6 -1
  241. package/src/inference/pipelines/text/logits/index.js +13 -12
  242. package/src/inference/pipelines/text/logits/utils.d.ts +7 -0
  243. package/src/inference/pipelines/text/logits/utils.js +9 -0
  244. package/src/inference/pipelines/text/lora-apply.js +50 -32
  245. package/src/inference/pipelines/text/model-load.js +282 -104
  246. package/src/inference/pipelines/text/moe-cache.js +5 -4
  247. package/src/inference/pipelines/text/moe-cpu-gptoss.js +74 -69
  248. package/src/inference/pipelines/text/moe-cpu.js +42 -38
  249. package/src/inference/pipelines/text/moe-gpu.js +110 -86
  250. package/src/inference/pipelines/text/ops.js +90 -90
  251. package/src/inference/pipelines/text/probes.js +9 -9
  252. package/src/inference/pipelines/text/sampling.js +52 -6
  253. package/src/inference/pipelines/text/weights.js +17 -7
  254. package/src/inference/pipelines/text.js +13 -1
  255. package/src/inference/speculative.d.ts +2 -2
  256. package/src/inference/speculative.js +4 -18
  257. package/src/inference/test-harness.d.ts +1 -1
  258. package/src/inference/test-harness.js +17 -7
  259. package/src/inference/tokenizer.d.ts +0 -5
  260. package/src/inference/tokenizer.js +4 -23
  261. package/src/inference/tokenizers/bpe.js +9 -0
  262. package/src/inference/tokenizers/bundled.js +20 -0
  263. package/src/inference/tokenizers/sentencepiece.js +12 -0
  264. package/src/loader/doppler-loader.js +38 -22
  265. package/src/loader/dtype-utils.js +3 -44
  266. package/src/loader/embedding-loader.js +7 -3
  267. package/src/loader/experts/expert-cache.js +13 -6
  268. package/src/loader/experts/expert-loader.js +10 -6
  269. package/src/loader/final-weights-loader.js +10 -4
  270. package/src/loader/layer-loader.js +2 -1
  271. package/src/loader/loader-state.js +2 -2
  272. package/src/loader/memory-monitor.js +8 -0
  273. package/src/loader/multi-model-loader.d.ts +14 -0
  274. package/src/loader/multi-model-loader.js +70 -24
  275. package/src/loader/shard-cache.js +84 -14
  276. package/src/loader/shard-resolver.js +25 -3
  277. package/src/loader/tensors/tensor-loader.js +214 -144
  278. package/src/loader/tensors/tensor-reader.js +76 -19
  279. package/src/loader/weight-downcast.js +1 -1
  280. package/src/memory/buffer-pool.d.ts +9 -1
  281. package/src/memory/buffer-pool.js +109 -44
  282. package/src/memory/unified-detect.js +1 -1
  283. package/src/rules/inference/dtype.rules.json +5 -0
  284. package/src/rules/inference/kernel-path.rules.json +24 -8
  285. package/src/rules/kernels/split-qg.rules.json +6 -0
  286. package/src/rules/rule-registry.js +27 -1
  287. package/src/storage/backends/opfs-store.js +68 -24
  288. package/src/storage/downloader.js +365 -83
  289. package/src/storage/index.d.ts +3 -0
  290. package/src/storage/index.js +3 -0
  291. package/src/storage/preflight.d.ts +2 -2
  292. package/src/storage/preflight.js +24 -2
  293. package/src/storage/quickstart-downloader.js +11 -5
  294. package/src/storage/registry.js +10 -4
  295. package/src/storage/reports.js +1 -1
  296. package/src/storage/shard-manager.d.ts +15 -1
  297. package/src/storage/shard-manager.js +55 -6
  298. package/src/storage/source-artifact-store.d.ts +52 -0
  299. package/src/storage/source-artifact-store.js +234 -0
  300. package/src/tooling/command-api-constants.d.ts +9 -0
  301. package/src/tooling/command-api-constants.js +9 -0
  302. package/src/tooling/command-api-family-normalizers.d.ts +9 -0
  303. package/src/tooling/command-api-family-normalizers.js +343 -0
  304. package/src/tooling/command-api-helpers.d.ts +25 -0
  305. package/src/tooling/command-api-helpers.js +262 -0
  306. package/src/tooling/command-api.js +16 -602
  307. package/src/tooling/command-envelope.js +4 -1
  308. package/src/tooling/command-runner-shared.js +52 -18
  309. package/src/tooling/conversion-config-materializer.js +3 -5
  310. package/src/tooling/lean-execution-contract.js +150 -3
  311. package/src/tooling/node-browser-command-runner.js +161 -271
  312. package/src/tooling/node-command-runner.js +29 -3
  313. package/src/tooling/node-converter.js +30 -1
  314. package/src/tooling/node-source-runtime.d.ts +1 -1
  315. package/src/tooling/node-source-runtime.js +120 -3
  316. package/src/tooling/node-webgpu.js +24 -21
  317. package/src/tooling/opfs-cache.js +21 -4
  318. package/src/tooling/runtime-input-composition.d.ts +38 -0
  319. package/src/tooling/runtime-input-composition.js +86 -0
  320. package/src/tooling/source-runtime-bundle.d.ts +40 -5
  321. package/src/tooling/source-runtime-bundle.js +261 -34
  322. package/src/tooling/source-runtime-materializer.d.ts +6 -0
  323. package/src/tooling/source-runtime-materializer.js +93 -0
  324. package/src/training/attention-backward.js +32 -17
  325. package/src/training/autograd.js +80 -52
  326. package/src/training/checkpoint-watch.d.ts +2 -1
  327. package/src/training/checkpoint-watch.js +39 -6
  328. package/src/training/checkpoint.js +40 -11
  329. package/src/training/clip.js +2 -1
  330. package/src/training/datasets/token-batch.js +20 -8
  331. package/src/training/distillation/checkpoint-watch.js +1 -0
  332. package/src/training/distillation/student-fixture.d.ts +22 -0
  333. package/src/training/distillation/student-fixture.js +846 -0
  334. package/src/training/distillation/suite-data.d.ts +45 -0
  335. package/src/training/distillation/suite-data.js +189 -0
  336. package/src/training/lora-pipeline.js +4 -7
  337. package/src/training/lora.js +26 -12
  338. package/src/training/loss.js +5 -6
  339. package/src/training/objectives/cross_entropy.js +2 -5
  340. package/src/training/objectives/distill_kd.js +4 -8
  341. package/src/training/objectives/distill_triplet.js +4 -8
  342. package/src/training/objectives/ul_stage2_base.js +4 -8
  343. package/src/training/operator-command.js +2 -0
  344. package/src/training/optimizer.js +19 -7
  345. package/src/training/runner.js +2 -1
  346. package/src/training/suite.js +18 -978
  347. package/src/training/tensor-factory.d.ts +9 -0
  348. package/src/training/tensor-factory.js +13 -0
  349. package/src/training/trainer.js +3 -5
  350. package/src/training/ul_dataset.js +3 -5
  351. package/src/training/workloads.js +70 -79
  352. package/src/types/model.d.ts +5 -0
  353. package/src/version.js +1 -1
  354. package/tools/convert-safetensors-node.js +22 -16
  355. package/tools/doppler-cli.js +50 -26
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dump intermediate values from Qwen3.5 linear attention (GatedDeltaNet) for comparison with Doppler.
4
+
5
+ Usage:
6
+ HF_HOME=/media/x/models/huggingface_cache python3 src/debug/reference/hf_qwen35_linear_attn_debug.py
7
+ """
8
+
9
+ import os
10
+ import torch
11
+ import numpy as np
12
+
13
+ os.environ.setdefault("HF_HOME", "/media/x/models/huggingface_cache")
14
+
15
+ from transformers import AutoModelForCausalLM, AutoTokenizer
16
+
17
+ MODEL_ID = "Qwen/Qwen3.5-0.8B"
18
+ PROMPT = "Hello"
19
+
20
+
21
+ def stats(name, tensor):
22
+ t = tensor.float().detach().flatten()
23
+ print(f" {name}: shape={list(tensor.shape)}, "
24
+ f"min={t.min().item():.6f}, max={t.max().item():.6f}, "
25
+ f"mean={t.mean().item():.6f}, absMax={t.abs().max().item():.6f}")
26
+ first8 = t[:8].tolist()
27
+ print(f" first8: {[f'{v:.6f}' for v in first8]}")
28
+
29
+
30
+ def main():
31
+ print(f"Loading {MODEL_ID}...")
32
+ model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.float32)
33
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
34
+ model.eval()
35
+
36
+ inputs = tokenizer(PROMPT, return_tensors="pt")
37
+ input_ids = inputs["input_ids"]
38
+ print(f"Prompt: '{PROMPT}', Token IDs: {input_ids[0].tolist()}")
39
+ num_tokens = input_ids.shape[1]
40
+
41
+ # Dump key weight values for layer 0
42
+ layer0 = model.model.layers[0]
43
+ attn = layer0.linear_attn
44
+
45
+ print(f"\n=== Layer 0 weights ===")
46
+ if hasattr(attn, 'A_log'):
47
+ a_log = attn.A_log.detach().float()
48
+ a_neg_exp = -torch.exp(a_log)
49
+ stats("A_log", a_log)
50
+ stats("a_neg_exp", a_neg_exp)
51
+ if hasattr(attn, 'dt_bias'):
52
+ stats("dt_bias", attn.dt_bias.detach().float())
53
+ stats("conv1d.weight", attn.conv1d.weight.detach().float())
54
+ stats("norm.weight", attn.norm.weight.detach().float())
55
+
56
+ # Hook into the linear_attn module to capture its input and output
57
+ captured = {}
58
+
59
+ def hook_linear_attn_input(module, args, kwargs):
60
+ if len(args) > 0:
61
+ captured['linear_attn_input'] = args[0].detach().clone()
62
+ return None
63
+
64
+ def hook_linear_attn_output(module, args, kwargs, output):
65
+ if isinstance(output, tuple):
66
+ captured['linear_attn_output'] = output[0].detach().clone()
67
+ else:
68
+ captured['linear_attn_output'] = output.detach().clone()
69
+ return None
70
+
71
+ # Hook into individual projection layers
72
+ def make_hook(name):
73
+ def hook(module, input, output):
74
+ captured[name] = output.detach().clone()
75
+ return hook
76
+
77
+ hooks = []
78
+ hooks.append(attn.register_forward_pre_hook(hook_linear_attn_input, with_kwargs=True))
79
+ hooks.append(attn.register_forward_hook(hook_linear_attn_output, with_kwargs=True))
80
+ hooks.append(attn.in_proj_qkv.register_forward_hook(make_hook('qkv_proj')))
81
+ hooks.append(attn.in_proj_z.register_forward_hook(make_hook('z_proj')))
82
+ hooks.append(attn.in_proj_a.register_forward_hook(make_hook('a_proj')))
83
+ hooks.append(attn.in_proj_b.register_forward_hook(make_hook('b_proj')))
84
+ hooks.append(attn.out_proj.register_forward_hook(make_hook('out_proj')))
85
+ hooks.append(attn.conv1d.register_forward_hook(make_hook('conv1d_raw')))
86
+ hooks.append(attn.norm.register_forward_hook(make_hook('gated_norm')))
87
+
88
+ # Also hook input_layernorm
89
+ hooks.append(layer0.input_layernorm.register_forward_hook(make_hook('input_layernorm')))
90
+
91
+ print(f"\n=== Running forward pass ===")
92
+ with torch.no_grad():
93
+ outputs = model(input_ids, output_hidden_states=True)
94
+
95
+ # Remove hooks
96
+ for h in hooks:
97
+ h.remove()
98
+
99
+ print(f"\n=== Captured intermediates ===")
100
+ for name in ['input_layernorm', 'qkv_proj', 'z_proj', 'a_proj', 'b_proj',
101
+ 'conv1d_raw', 'gated_norm', 'linear_attn_input', 'linear_attn_output', 'out_proj']:
102
+ if name in captured:
103
+ stats(name, captured[name])
104
+ else:
105
+ print(f" {name}: NOT CAPTURED")
106
+
107
+ # Hidden states per layer
108
+ print(f"\n=== Hidden states per layer (last token) ===")
109
+ for i in range(min(6, len(outputs.hidden_states) - 1)):
110
+ hs = outputs.hidden_states[i + 1]
111
+ t = hs[0, -1] # last token
112
+ vals = t[:8].tolist()
113
+ max_abs = t.abs().max().item()
114
+ mean_abs = t.abs().mean().item()
115
+ layer_type = type(model.model.layers[i]).__name__
116
+ attn_type = "linear" if hasattr(model.model.layers[i], 'linear_attn') else "full"
117
+ print(f" Layer {i} ({attn_type}): first8={[f'{v:.4f}' for v in vals]}, "
118
+ f"maxAbs={max_abs:.4f}, meanAbs={mean_abs:.4f}")
119
+
120
+ # Logits
121
+ logits = outputs.logits[0, -1]
122
+ top5 = torch.topk(logits, 5)
123
+ print(f"\nTop-5 logits: {[(tokenizer.decode([idx.item()]), f'{val.item():.2f}') for val, idx in zip(top5.values, top5.indices)]}")
124
+
125
+ # Also trace through the linear attention manually to compare with Doppler's kernel
126
+ print(f"\n=== Manual linear attention trace (layer 0) ===")
127
+ with torch.no_grad():
128
+ embed = model.model.embed_tokens(input_ids)
129
+ normed = layer0.input_layernorm(embed)
130
+ stats("normed_input", normed)
131
+
132
+ qkv = attn.in_proj_qkv(normed)
133
+ stats("qkv", qkv)
134
+
135
+ # The HF Qwen3.5 GatedDeltaNet does conv1d on the QKV, then applies SiLU
136
+ # The conv1d expects [batch, channels, seq_len] format
137
+ qkv_t = qkv.transpose(1, 2) # [1, 6144, 1]
138
+
139
+ # Use the conv1d module directly (it has padding configured)
140
+ conv_raw = attn.conv1d(qkv_t)
141
+ stats("conv_raw (from module)", conv_raw.transpose(1, 2))
142
+
143
+ # Truncate to seq_len (causal conv padding)
144
+ conv_causal = conv_raw[..., :num_tokens]
145
+ stats("conv_causal (truncated)", conv_causal.transpose(1, 2))
146
+
147
+ # Apply SiLU
148
+ conv_silu = torch.nn.functional.silu(conv_causal)
149
+ stats("conv_silu", conv_silu.transpose(1, 2))
150
+
151
+ # Split Q, K, V
152
+ conv_out = conv_silu.transpose(1, 2) # [1, seq_len, 6144]
153
+ num_k_heads = 16
154
+ head_k_dim = 128
155
+ head_v_dim = 128
156
+ num_v_heads = 16
157
+ q_size = num_k_heads * head_k_dim # 2048
158
+ k_size = q_size
159
+ v_size = num_v_heads * head_v_dim # 2048
160
+
161
+ q = conv_out[..., :q_size]
162
+ k = conv_out[..., q_size:q_size + k_size]
163
+ v = conv_out[..., q_size + k_size:]
164
+ stats("Q (raw)", q)
165
+ stats("K (raw)", k)
166
+ stats("V (raw)", v)
167
+
168
+ # Reshape for per-head processing
169
+ # Q and K: [batch, seq, num_k_heads, head_k_dim]
170
+ q_heads = q.view(1, num_tokens, num_k_heads, head_k_dim)
171
+ k_heads = k.view(1, num_tokens, num_k_heads, head_k_dim)
172
+ v_heads = v.view(1, num_tokens, num_v_heads, head_v_dim)
173
+
174
+ # L2 normalize Q and K
175
+ eps = 1e-6
176
+ q_norm = torch.nn.functional.normalize(q_heads, p=2, dim=-1, eps=eps)
177
+ k_norm = torch.nn.functional.normalize(k_heads, p=2, dim=-1, eps=eps)
178
+
179
+ # Scale Q by 1/sqrt(head_k_dim)
180
+ head_scale = 1.0 / (head_k_dim ** 0.5)
181
+ q_scaled = q_norm * head_scale
182
+
183
+ stats("Q_normed_scaled (per-head)", q_scaled.reshape(1, num_tokens, -1))
184
+ stats("K_normed (per-head)", k_norm.reshape(1, num_tokens, -1))
185
+
186
+ # Projections for gating
187
+ z = attn.in_proj_z(normed)
188
+ a_out = attn.in_proj_a(normed)
189
+ b_out = attn.in_proj_b(normed)
190
+ stats("z", z)
191
+ stats("a", a_out)
192
+ stats("b", b_out)
193
+
194
+ # Compute gating values
195
+ a_log = attn.A_log.detach().float()
196
+ a_neg_exp = -torch.exp(a_log)
197
+ dt_bias = attn.dt_bias.detach().float()
198
+
199
+ softplus_input = a_out.squeeze(0).squeeze(0) + dt_bias
200
+ softplus_val = torch.nn.functional.softplus(softplus_input)
201
+ g = a_neg_exp * softplus_val
202
+ g_exp = torch.exp(g)
203
+ beta = torch.sigmoid(b_out.squeeze(0).squeeze(0))
204
+
205
+ stats("softplus(a + dt_bias)", softplus_val.unsqueeze(0).unsqueeze(0))
206
+ stats("g (decay)", g.unsqueeze(0).unsqueeze(0))
207
+ stats("g_exp (decay factor)", g_exp.unsqueeze(0).unsqueeze(0))
208
+ stats("beta (sigmoid(b))", beta.unsqueeze(0).unsqueeze(0))
209
+
210
+ # Recurrent state update (for first token, state is all zeros)
211
+ # state[head, kd, vd] = state * g_exp + k[kd] * delta[vd]
212
+ # where delta[vd] = (v[vd] - state^T @ k * beta
213
+ # For zero state: delta[vd] = v[vd] * beta, state = k ⊗ delta
214
+ state = torch.zeros(num_v_heads, head_k_dim, head_v_dim)
215
+
216
+ # Apply decay (no-op for zero state)
217
+ for head in range(num_v_heads):
218
+ state[head] *= g_exp[head].item()
219
+
220
+ k_head = k_norm[0, 0, head % num_k_heads] # broadcast q_rep
221
+ v_head = v_heads[0, 0, head]
222
+
223
+ # kv_mem = state @ k
224
+ kv_mem = state[head].t() @ k_head # [head_v_dim]
225
+
226
+ # delta = (v - kv_mem) * beta
227
+ delta = (v_head - kv_mem) * beta[head].item()
228
+
229
+ # state += outer(k, delta)
230
+ state[head] += torch.outer(k_head, delta)
231
+
232
+ # Output: out = state^T @ q
233
+ output_per_head = torch.zeros(1, num_tokens, num_v_heads, head_v_dim)
234
+ for head in range(num_v_heads):
235
+ q_head = q_scaled[0, 0, head % num_k_heads]
236
+ out_head = state[head].t() @ q_head # [head_v_dim]
237
+ output_per_head[0, 0, head] = out_head
238
+
239
+ raw_out = output_per_head.reshape(1, num_tokens, num_v_heads * head_v_dim)
240
+ stats("Recurrent output (raw)", raw_out)
241
+
242
+ # RMS norm per head + SiLU gate
243
+ z_reshaped = z.view(1, num_tokens, num_v_heads, head_v_dim)
244
+ norm_weight = attn.norm.weight.detach().float() # [head_v_dim] (shared mode)
245
+ rms_eps = 1e-6
246
+
247
+ for head in range(num_v_heads):
248
+ head_out = output_per_head[0, 0, head] # [head_v_dim]
249
+ mean_sq = (head_out ** 2).mean()
250
+ inv_rms = 1.0 / torch.sqrt(mean_sq + rms_eps)
251
+ z_gate = torch.nn.functional.silu(z_reshaped[0, 0, head])
252
+ output_per_head[0, 0, head] = head_out * inv_rms * norm_weight * z_gate
253
+
254
+ gated_out = output_per_head.reshape(1, num_tokens, num_v_heads * head_v_dim)
255
+ stats("After RMSNorm + SiLU gate", gated_out)
256
+
257
+ # Output projection
258
+ o_result = torch.nn.functional.linear(gated_out, attn.out_proj.weight)
259
+ stats("After out_proj", o_result)
260
+
261
+ # Compare with captured output
262
+ if 'linear_attn_output' in captured:
263
+ diff = (o_result - captured['linear_attn_output']).abs()
264
+ print(f"\n Diff vs captured output: maxDiff={diff.max().item():.6f}")
265
+
266
+
267
+ if __name__ == "__main__":
268
+ main()
@@ -24,7 +24,13 @@ export function signalResult(data) {
24
24
 
25
25
 
26
26
  export function signalError(error, details) {
27
- console.log(`${SIGNALS.ERROR} ${JSON.stringify({ error, ...details })}`);
27
+ if (details != null && (typeof details !== 'object' || Array.isArray(details))) {
28
+ throw new Error('signalError details must be an object when provided.');
29
+ }
30
+ if (details && Object.hasOwn(details, 'error')) {
31
+ throw new Error('signalError details.error is reserved. Pass the primary error as the first argument.');
32
+ }
33
+ console.log(`${SIGNALS.ERROR} ${JSON.stringify({ error, ...(details ?? {}) })}`);
28
34
  }
29
35
 
30
36
 
@@ -63,6 +63,8 @@ export interface TensorInspectOptions {
63
63
  }
64
64
 
65
65
  export interface TensorSnapshot {
66
+ ok: boolean;
67
+ error: string | null;
66
68
  shape: number[];
67
69
  dtype: string;
68
70
  stats: {
@@ -202,7 +202,13 @@ export const tensor = {
202
202
 
203
203
  export async function snapshotTensor(buffer, shape, dtype = 'f32') {
204
204
  try {
205
- if (!gpuDevice) {
205
+ if (
206
+ !gpuDevice
207
+ || typeof gpuDevice.createBuffer !== 'function'
208
+ || typeof gpuDevice.createCommandEncoder !== 'function'
209
+ || !gpuDevice.queue
210
+ || typeof gpuDevice.queue.submit !== 'function'
211
+ ) {
206
212
  throw new Error('GPU device not initialized');
207
213
  }
208
214
  const elementSize = dtype === 'f16' ? 2 : 4;
@@ -224,8 +230,11 @@ export async function snapshotTensor(buffer, shape, dtype = 'f32') {
224
230
  staging.destroy();
225
231
  const arr = new Float32Array(data);
226
232
  return snapshotFromArray(arr, shape ?? [arr.length], dtype);
227
- } catch {
233
+ } catch (error) {
234
+ const message = error instanceof Error ? error.message : String(error);
228
235
  return {
236
+ ok: false,
237
+ error: message,
229
238
  shape: shape ?? [0],
230
239
  dtype,
231
240
  stats: { min: 0, max: 0, maxAbs: 0, mean: 0, std: 0 },
@@ -241,6 +250,8 @@ export function snapshotFromArray(arr, shape, dtype = 'f32') {
241
250
  const stats = computeArrayStats(arr, Math.min(arr.length, numElements));
242
251
 
243
252
  return {
253
+ ok: true,
254
+ error: null,
244
255
  shape,
245
256
  dtype,
246
257
  stats: {
@@ -38,10 +38,17 @@ function asOptionalTimestamp(value, label) {
38
38
  return Math.floor(parsed);
39
39
  }
40
40
 
41
- function asNonNegativeInteger(value, fallback) {
41
+ function asOptionalNonNegativeInteger(value, label) {
42
+ if (value === undefined || value === null) {
43
+ return null;
44
+ }
42
45
  const parsed = Number(value);
43
46
  if (!Number.isInteger(parsed) || parsed < 0) {
44
- return fallback;
47
+ throw createP2PTransportError(
48
+ P2P_TRANSPORT_ERROR_CODES.payloadInvalid,
49
+ `${label} must be a non-negative integer when provided.`,
50
+ { label }
51
+ );
45
52
  }
46
53
  return parsed;
47
54
  }
@@ -104,12 +111,11 @@ export function normalizeControlPlaneSessionUpdate(value, label = 'p2p control-p
104
111
 
105
112
  export function normalizeP2PPolicyDecision(value, label = 'p2p control-plane policy decision') {
106
113
  if (value === undefined || value === null) {
107
- return {
108
- allow: true,
109
- reason: null,
110
- sessionUpdate: null,
111
- metadata: null,
112
- };
114
+ throw createP2PTransportError(
115
+ P2P_TRANSPORT_ERROR_CODES.payloadInvalid,
116
+ `${label} must return an explicit boolean or object decision.`,
117
+ { label }
118
+ );
113
119
  }
114
120
 
115
121
  if (typeof value === 'boolean') {
@@ -129,9 +135,40 @@ export function normalizeP2PPolicyDecision(value, label = 'p2p control-plane pol
129
135
  );
130
136
  }
131
137
 
132
- const allow = value.allow === false || value.deny === true
133
- ? false
134
- : true;
138
+ const hasAllow = Object.prototype.hasOwnProperty.call(value, 'allow');
139
+ const hasDeny = Object.prototype.hasOwnProperty.call(value, 'deny');
140
+ if (!hasAllow && !hasDeny) {
141
+ throw createP2PTransportError(
142
+ P2P_TRANSPORT_ERROR_CODES.payloadInvalid,
143
+ `${label} must include allow or deny.`,
144
+ { label }
145
+ );
146
+ }
147
+ if (hasAllow && typeof value.allow !== 'boolean') {
148
+ throw createP2PTransportError(
149
+ P2P_TRANSPORT_ERROR_CODES.payloadInvalid,
150
+ `${label}.allow must be a boolean when provided.`,
151
+ { label }
152
+ );
153
+ }
154
+ if (hasDeny && typeof value.deny !== 'boolean') {
155
+ throw createP2PTransportError(
156
+ P2P_TRANSPORT_ERROR_CODES.payloadInvalid,
157
+ `${label}.deny must be a boolean when provided.`,
158
+ { label }
159
+ );
160
+ }
161
+ if (hasAllow && hasDeny && value.allow === value.deny) {
162
+ throw createP2PTransportError(
163
+ P2P_TRANSPORT_ERROR_CODES.payloadInvalid,
164
+ `${label} has conflicting allow/deny values.`,
165
+ { label }
166
+ );
167
+ }
168
+
169
+ const allow = hasAllow
170
+ ? value.allow
171
+ : value.deny !== true;
135
172
  const reason = asOptionalString(value.reason, `${label}.reason`);
136
173
  const sessionUpdate = normalizeControlPlaneSessionUpdate(
137
174
  {
@@ -180,7 +217,10 @@ export function normalizeP2PControlPlaneConfig(config = {}) {
180
217
  contractVersion: assertSupportedP2PControlPlaneContract(
181
218
  raw.contractVersion ?? P2P_CONTROL_PLANE_CONTRACT_VERSION
182
219
  ),
183
- tokenRefreshSkewMs: asNonNegativeInteger(raw.tokenRefreshSkewMs, DEFAULT_TOKEN_REFRESH_SKEW_MS),
220
+ tokenRefreshSkewMs: asOptionalNonNegativeInteger(
221
+ raw.tokenRefreshSkewMs,
222
+ 'p2p.controlPlane.tokenRefreshSkewMs'
223
+ ) ?? DEFAULT_TOKEN_REFRESH_SKEW_MS,
184
224
  tokenProvider,
185
225
  policyEvaluator,
186
226
  };
@@ -12,6 +12,14 @@ function asFiniteNumber(value, fallback = 0) {
12
12
  return Number.isFinite(parsed) ? parsed : fallback;
13
13
  }
14
14
 
15
+ function assertFiniteNumber(value, label) {
16
+ const parsed = Number(value);
17
+ if (!Number.isFinite(parsed)) {
18
+ throw new Error(`P2P observability ${label} must be a finite number.`);
19
+ }
20
+ return parsed;
21
+ }
22
+
15
23
  function asNonNegativeInteger(value, fallback = 0) {
16
24
  const parsed = Number(value);
17
25
  if (!Number.isInteger(parsed) || parsed < 0) {
@@ -69,15 +77,43 @@ function percentile(values, ratio) {
69
77
  }
70
78
 
71
79
  function resolveSLOTargets(options = {}) {
72
- const targets = options.targets && typeof options.targets === 'object'
73
- ? options.targets
74
- : {};
80
+ const hasExplicitTargets = Object.hasOwn(options, 'targets');
81
+ if (hasExplicitTargets && (options.targets == null || typeof options.targets !== 'object' || Array.isArray(options.targets))) {
82
+ throw new Error('P2P observability targets must be an object when provided.');
83
+ }
84
+ const targets = hasExplicitTargets ? options.targets : {};
85
+
86
+ const minAvailability = Object.hasOwn(targets, 'minAvailability')
87
+ ? assertFiniteNumber(targets.minAvailability, 'targets.minAvailability')
88
+ : DEFAULT_SLO_TARGETS.minAvailability;
89
+ const minP2PHitRate = Object.hasOwn(targets, 'minP2PHitRate')
90
+ ? assertFiniteNumber(targets.minP2PHitRate, 'targets.minP2PHitRate')
91
+ : DEFAULT_SLO_TARGETS.minP2PHitRate;
92
+ const maxHttpFallbackRate = Object.hasOwn(targets, 'maxHttpFallbackRate')
93
+ ? assertFiniteNumber(targets.maxHttpFallbackRate, 'targets.maxHttpFallbackRate')
94
+ : DEFAULT_SLO_TARGETS.maxHttpFallbackRate;
95
+ const maxP95LatencyMs = Object.hasOwn(targets, 'maxP95LatencyMs')
96
+ ? assertFiniteNumber(targets.maxP95LatencyMs, 'targets.maxP95LatencyMs')
97
+ : DEFAULT_SLO_TARGETS.maxP95LatencyMs;
98
+
99
+ if (minAvailability < 0 || minAvailability > 1) {
100
+ throw new Error('P2P observability targets.minAvailability must be between 0 and 1.');
101
+ }
102
+ if (minP2PHitRate < 0 || minP2PHitRate > 1) {
103
+ throw new Error('P2P observability targets.minP2PHitRate must be between 0 and 1.');
104
+ }
105
+ if (maxHttpFallbackRate < 0 || maxHttpFallbackRate > 1) {
106
+ throw new Error('P2P observability targets.maxHttpFallbackRate must be between 0 and 1.');
107
+ }
108
+ if (maxP95LatencyMs < 0) {
109
+ throw new Error('P2P observability targets.maxP95LatencyMs must be >= 0.');
110
+ }
75
111
 
76
112
  return {
77
- minAvailability: asFiniteNumber(targets.minAvailability, DEFAULT_SLO_TARGETS.minAvailability),
78
- minP2PHitRate: asFiniteNumber(targets.minP2PHitRate, DEFAULT_SLO_TARGETS.minP2PHitRate),
79
- maxHttpFallbackRate: asFiniteNumber(targets.maxHttpFallbackRate, DEFAULT_SLO_TARGETS.maxHttpFallbackRate),
80
- maxP95LatencyMs: asFiniteNumber(targets.maxP95LatencyMs, DEFAULT_SLO_TARGETS.maxP95LatencyMs),
113
+ minAvailability,
114
+ minP2PHitRate,
115
+ maxHttpFallbackRate,
116
+ maxP95LatencyMs,
81
117
  };
82
118
  }
83
119
 
@@ -200,6 +200,16 @@ function assertOpenDataChannel(channel, peerId) {
200
200
  }
201
201
 
202
202
  function toRequestMessage(requestId, context) {
203
+ if (context?.contractVersion !== P2P_WEBRTC_DATA_PLANE_CONTRACT_VERSION) {
204
+ throw createP2PTransportError(
205
+ P2P_TRANSPORT_ERROR_CODES.payloadInvalid,
206
+ `Unexpected WebRTC data-plane contractVersion "${context?.contractVersion}".`,
207
+ {
208
+ expectedContractVersion: P2P_WEBRTC_DATA_PLANE_CONTRACT_VERSION,
209
+ actualContractVersion: context?.contractVersion ?? null,
210
+ }
211
+ );
212
+ }
203
213
  return {
204
214
  schemaVersion: P2P_WEBRTC_MESSAGE_SCHEMA_VERSION,
205
215
  contractVersion: P2P_WEBRTC_DATA_PLANE_CONTRACT_VERSION,
@@ -377,6 +387,16 @@ export function createBrowserWebRTCDataPlaneTransport(config = {}) {
377
387
  const maxPayloadBytes = Math.max(1, asNonNegativeInteger(config.maxPayloadBytes, DEFAULT_MAX_PAYLOAD_BYTES));
378
388
 
379
389
  return async function webRtcDataPlaneTransport(context) {
390
+ if (context?.contractVersion !== P2P_WEBRTC_DATA_PLANE_CONTRACT_VERSION) {
391
+ throw createP2PTransportError(
392
+ P2P_TRANSPORT_ERROR_CODES.contractUnsupported,
393
+ `Unsupported p2p.webrtc contractVersion "${context?.contractVersion}". Supported: ${P2P_WEBRTC_DATA_PLANE_CONTRACT_VERSION}.`,
394
+ {
395
+ contractVersion: context?.contractVersion ?? null,
396
+ }
397
+ );
398
+ }
399
+
380
400
  const selection = normalizePeerSelectionResult(
381
401
  selectPeer ? await selectPeer(context) : { peerId: staticPeerId }
382
402
  );