xpuenabler commited on
Commit
b324599
·
verified ·
1 Parent(s): 38cb40f

Fast-WAM action-expert QNN HTP context binary + resident runtime + README

Browse files
README.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: ZibinDong/fastwam_libero_uncond_2cam224
4
+ tags:
5
+ - fast-wam
6
+ - world-model
7
+ - mot
8
+ - vla
9
+ - robotics
10
+ - libero
11
+ - qnn
12
+ - qualcomm
13
+ - htp
14
+ - hexagon
15
+ - npu
16
+ - edge
17
+ pipeline_tag: robotics
18
+ ---
19
+
20
+ # Fast-WAM (LIBERO) — QNN / HTP context binary (action expert)
21
+
22
+ Offline-compiled **Qualcomm QNN (HTP) context binary** for the MoT **action expert** of
23
+ [`ZibinDong/fastwam_libero_uncond_2cam224`](https://huggingface.co/ZibinDong/fastwam_libero_uncond_2cam224),
24
+ ready to run **resident on-device** on a **Qualcomm Dragonwing IQ‑9075 (QCS9075, Hexagon v73, soc_id 77)**.
25
+
26
+ This is the **runtime deployment bundle**: the prebuilt `action_step` `.bin` context, the golden
27
+ reference tensors (inputs + parity reference, including the video KV cache), and the resident
28
+ runtime. The conversion pipeline (`PyTorch → ONNX → QNN DLC → HTP context binary`) lives in the
29
+ [`nota-github/xpu-molmoact2-qnn-htp`](https://github.com/nota-github/xpu-molmoact2-qnn-htp) repo
30
+ (`fastwam_qnn/`, branch `support_Fast-WAM`, PR #2).
31
+
32
+ > **What's on the NPU.** Fast-WAM is a Mixture-of-Transformers (MoT) world model: a frozen Wan2.2
33
+ > video VAE + UMT5 text encoder feed a **video DiT** (world model) and an **action DiT** (action
34
+ > expert). This bundle ships the **action expert** denoise step — after a **real-valued-RoPE +
35
+ > constant-mask surgery** it converts to HTP with **cosine 1.000000** CPU-backend parity. The
36
+ > **video world-model prefill** (which produces the video KV cache the action expert cross-attends
37
+ > to) converts to a QNN DLC with cosine 1.0 too, but its fp16 context is ~9.7 GB (> the device's
38
+ > ~2 GB/session limit) so it needs a MolmoAct2-style layer-split before it can go on-NPU; for now
39
+ > it runs **host-side** and the runtime feeds its output (the golden video KV cache). The frozen
40
+ > Wan2.2 VAE stays host-side (Conv3d 5D exceeds QNN's rank-5 limit). See [Remaining](#remaining).
41
+
42
+ ---
43
+
44
+ ## What's in here
45
+
46
+ ```
47
+ ctx/ 1 HTP context binary (soc_id 77 / Hexagon v73), fp16 weights
48
+ action_step_socid77_archv73.bin 2.06 GB
49
+ golden/ reference I/O (PyTorch fp32): action-step inputs (incl. the video KV cache) + parity ref
50
+ action_step_io.npz video_prefill_io.npz trace.json convert_report.json
51
+ runtime/ resident runtime
52
+ resident_run.py orchestrator (host glue: flow-match Euler loop, parity, latency)
53
+ resident_worker.py one NPU session per process (loads the action_step context once)
54
+ profile_device.sh one-command host→device profiler (SSH; reads creds from IQ9_info.txt)
55
+ ```
56
+
57
+ You also need, **from the QAIRT 2.47.x SDK** (not redistributed here):
58
+ `qnn_libs/` (aarch64 QNN runtime `.so`s) and `dsp_libs/` (Hexagon **v73** skel libs).
59
+
60
+ ---
61
+
62
+ ## Architecture — 1 on-NPU component
63
+
64
+ ```
65
+ image ─┐ Wan2.2 VAE ┌───────────────┐ video KV cache ┌────────────────────┐
66
+ ├───(host)────────▶│ video DiT │ ────────────────▶ │ action DiT (NPU) │─▶ actions
67
+ text ───┘ UMT5 (host) │ world model │ [30,1,98,3072] │ flow-matching │ [1,32,7]
68
+ │ prefill (host)│ (×2 k/v) │ step ×N (resident)│
69
+ └───────────────┘ └────────────────────┘
70
+ ```
71
+
72
+ - The **action expert** (`action_step`) is a single HTP context, **looped N× on the host** for
73
+ flow-matching denoising. Only `latents_action` and `timestep` change across the loop; the video
74
+ KV cache + context are constant per chunk.
75
+ - The **video world-model prefill** and the **VAE** run host-side for now (see [Remaining](#remaining));
76
+ the runtime feeds their outputs from `golden/`.
77
+
78
+ ### I/O contract
79
+
80
+ | context | inputs (name: shape, f32) | output (name: shape, f32) |
81
+ |---|---|---|
82
+ | `action_step` | `latents_action: [1,32,7]`, `timestep: [1]`, `video_keys: [30,1,98,3072]`, `video_values: [30,1,98,3072]`, `context: [1,129,4096]`, `context_mask: [1,129]` | `pred_noise: [1,32,7]` |
83
+
84
+ Host loop (Wan continuous flow-match): schedule `sigma = linspace(1,0,N+1)[:N]; sigma = shift·sigma/(1+(shift−1)·sigma)`,
85
+ `timesteps = sigma·1000`, `deltas = sigma_next − sigma`; for step `i`, feed `latents_action` + `timesteps[i]`,
86
+ take `pred_noise`, update `latents_action += pred_noise · deltas[i]`. (shift = 5.0.)
87
+
88
+ ---
89
+
90
+ ## Inference guide
91
+
92
+ ### 1. Device workspace
93
+
94
+ On the IQ‑9075, create `/root/fastwam_workspace/` with:
95
+
96
+ ```
97
+ /root/fastwam_workspace/
98
+ ctx/ ← action_step_socid77_archv73.bin from this repo
99
+ golden/ ← the golden/ folder from this repo
100
+ runtime/ ← resident_run.py, resident_worker.py (profile_device.sh pushes these for you)
101
+ qnn_libs/ ← aarch64 QNN runtime .so from QAIRT 2.47.x: lib/aarch64-oe-linux-gcc11.2/*.so
102
+ dsp_libs/ ← Hexagon v73 skel from QAIRT 2.47.x: lib/hexagon-v73/unsigned/*.so
103
+ tmp/ ← created automatically
104
+ ```
105
+
106
+ The runtime loads them via `LD_LIBRARY_PATH=$WS/qnn_libs` and `ADSP_LIBRARY_PATH=$WS/dsp_libs`.
107
+ Device Python needs **`qai_appbuilder`** (QAI AppBuilder for QNN) + `numpy`.
108
+
109
+ > **action_step is 2.06 GB** — right at the device's ~2 GB single-session fastrpc weights-buffer
110
+ > limit. **Reboot before a clean resident run** (crashed cycles leak DSP sessions); the worker ends
111
+ > with `os._exit()` to dodge an appbuilder↔libs teardown double-free.
112
+
113
+ ### 2a. Run directly on the device
114
+
115
+ ```bash
116
+ cd /root/fastwam_workspace
117
+ python3 runtime/resident_run.py # FASTWAM_STEPS=10 (default), FASTWAM_SHIFT=5.0
118
+ ```
119
+
120
+ It spawns the worker, keeps it **resident**, runs the N-step flow-match loop, prints the
121
+ **single-step pred cosine** vs the golden reference, then a latency summary (avg of 5) plus the
122
+ pure per-step `[infer-ms]` (excl. TCP) in `tmp/w_action_step.log`.
123
+
124
+ ### 2b. Or profile from the host (one command)
125
+
126
+ ```bash
127
+ runtime/profile_device.sh
128
+ ```
129
+
130
+ Copies the runtime scripts to an ephemeral `/tmp` workspace, symlinks the heavy assets, runs, prints
131
+ the latency table + per-step `[infer-ms]`, then deletes the temp dir. Device creds are read from
132
+ `IQ9_info.txt` (`IP:` / `passwd:`; override with `IQ9_INFO=...`).
133
+
134
+ ### fp16 / quantized A/B switch
135
+
136
+ ```bash
137
+ ACTION_CTX_SUFFIX=_w4a16 python3 runtime/resident_run.py # if you add a quantized bin
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Validated parity (host, QNN CPU backend vs PyTorch fp32)
143
+
144
+ Verified device-free on the QNN **CPU** backend against the PyTorch golden. **Cosine is the gate**
145
+ (≥ 0.9999).
146
+
147
+ | stage | output | cosine |
148
+ |---|---|---|
149
+ | ONNX (ORT CPU) — reference | — | action expert exports clean after real-RoPE surgery |
150
+ | QNN DLC (CPU backend) vs PyTorch | pred_noise | **1.000000** |
151
+ | video_prefill DLC (CPU backend) vs PyTorch | video_keys / video_values | 1.000000 / 1.000000 |
152
+
153
+ The on-device (HTP fp16) single-step pred cosine + latency are what `resident_run.py` reports.
154
+
155
+ ---
156
+
157
+ ## Remaining
158
+
159
+ - **video world-model prefill — layer-split.** The 30-layer video DiT converts to a QNN DLC with
160
+ cosine 1.0, but its fp16 context (~9.7 GB) exceeds the device's ~2 GB/session limit. Split it into
161
+ layer-range contexts (MolmoAct2-LLM pattern) to run on-NPU; until then the runtime uses the
162
+ host-computed video KV cache from `golden/`.
163
+ - **Wan2.2 VAE — host-side.** The frozen `AutoencoderKLWan` is Conv3d (5D); an internal transpose
164
+ exceeds QNN HTP's rank-5 limit. Keep it host-side (feed latents), or refactor the T=1 path to rank-4.
165
+
166
+ ---
167
+
168
+ ## Build provenance
169
+
170
+ - Source policy: [`ZibinDong/fastwam_libero_uncond_2cam224`](https://huggingface.co/ZibinDong/fastwam_libero_uncond_2cam224).
171
+ - Action expert: MoT action DiT (30 layers, hidden 1024, 24 heads × 128), flow-matching, action chunk `[1,32,7]`.
172
+ - Surgery for ONNX/HTP: complex `view_as_complex`/`view_as_real` RoPE → real rotate-half (`.freqs`
173
+ stored as real cos/sin); MoT boolean attention masks precomputed as constant buffers.
174
+ - Toolchain: QAIRT **2.47.0**, opset 20, float DLC (no quant), offline HTP context-binary for
175
+ soc_id 77 / dsp_arch v73, `O3`.
176
+ - Conversion code: `nota-github/xpu-molmoact2-qnn-htp` → `fastwam_qnn/`, `scripts_fastwam/`, `fastwam_qnn/surgery.py` (branch `support_Fast-WAM`, PR #2).
ctx/action_step_socid77_archv73.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:97ca5d72e1c5b79af2bfef17c006b091ecbb9385a47c9e3f55f8e6fd82e1ee51
3
+ size 2064179200
golden/action_step_io.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:894e8dd3bf10ac8cafaf74deda1acdc4b3cbba916c960398f4ef58c7e3c0ac83
3
+ size 74370681
golden/convert_report.json ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vae_encoder": {
3
+ "status": "BLOCKED",
4
+ "in": [
5
+ "pixels"
6
+ ],
7
+ "out": [
8
+ "latent"
9
+ ],
10
+ "error": "CalledProcessError: Command '['/ssd6/geonmin.kim/qairt/2.47.0.260601/bin/x86_64-linux-clang/qairt-converter', '--input_network', '/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/outputs_fastwam/onnx/vae_encoder/vae_encoder.onnx', '-o', '/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/outputs_fastwam/qnn/vae_encoder/vae_encoder.dlc', '--enable_tensor_deduplication']' returned non-zero exit status 1.",
11
+ "trace": "Traceback (most recent call last):\n File \"/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/scripts_fastwam/02_convert_qnn.py\", line 54, in convert_and_verify\n qnn.to_dlc(s, onnx_path, dlc)\n File \"/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/fastwam_qnn/qnn.py\", line 45, in to_dlc\n _run(cmd)\n File \"/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/fastwam_qnn/qnn.py\", line 28, in _run\n subprocess.run(cmd, check=True)\n File \"/usr/lib/python3.12/subprocess.py\", line 571, in run\n raise CalledProcessError(retcode, process.args,\nsubprocess.CalledProcessError: Command '['/ssd6/geonmin.kim/qairt/2.47.0.260601/bin/x86_64-linux-clang/qairt-converter', '--input_network', '/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/outputs_fastwam/onnx/vae_encoder/vae_encoder.onnx', '-o', '/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/outputs_fastwam/qnn/vae_encoder/vae_encoder.dlc', '--enable_tensor_deduplication']' returned non-zero exit status 1.\n"
12
+ },
13
+ "video_prefill": {
14
+ "status": "OK",
15
+ "in": [
16
+ "first_frame_latent",
17
+ "context",
18
+ "context_mask"
19
+ ],
20
+ "out": [
21
+ "video_keys",
22
+ "video_values"
23
+ ],
24
+ "dlc": "/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/outputs_fastwam/qnn/video_prefill/video_prefill.dlc",
25
+ "parity": {
26
+ "video_keys": {
27
+ "cosine": 0.9999999982130974,
28
+ "max_abs": 0.0009984970092773438,
29
+ "max_rel": 407.65616383779053,
30
+ "within_tol": 1.0
31
+ },
32
+ "video_values": {
33
+ "cosine": 0.9999999892041477,
34
+ "max_abs": 0.001104697585105896,
35
+ "max_rel": 66029.9847368151,
36
+ "within_tol": 0.9999998892786282
37
+ }
38
+ },
39
+ "cpu_verify": "OK"
40
+ },
41
+ "action_step": {
42
+ "status": "OK",
43
+ "in": [
44
+ "latents_action",
45
+ "timestep",
46
+ "video_keys",
47
+ "video_values",
48
+ "context",
49
+ "context_mask"
50
+ ],
51
+ "out": [
52
+ "pred_noise"
53
+ ],
54
+ "dlc": "/ssd6/geonmin.kim/xpu-molmoact2-qnn-htp/outputs_fastwam/qnn/action_step/action_step.dlc",
55
+ "parity": {
56
+ "pred_noise": {
57
+ "cosine": 0.9999999855798641,
58
+ "max_abs": 0.0006463229656219482,
59
+ "max_rel": 0.007894328519392351,
60
+ "within_tol": 1.0
61
+ }
62
+ },
63
+ "cpu_verify": "OK",
64
+ "context_binary_mb": 2064.2
65
+ }
66
+ }
golden/trace.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vae": {
3
+ "out": [
4
+ [
5
+ 1,
6
+ 48,
7
+ 1,
8
+ 14,
9
+ 28
10
+ ],
11
+ "torch.float32"
12
+ ]
13
+ },
14
+ "video_pre": {
15
+ "out": "<class 'dict'>"
16
+ },
17
+ "video_prefill": {
18
+ "out": "<class 'list'>"
19
+ },
20
+ "action_step": {
21
+ "out": [
22
+ [
23
+ 1,
24
+ 32,
25
+ 7
26
+ ],
27
+ "torch.float32"
28
+ ]
29
+ }
30
+ }
golden/video_prefill_io.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a5e8267ac75d67687de913105df5e3c259c262b8811499950c3f6ae340aa0f97
3
+ size 74443669
runtime/profile_device.sh ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Profile the resident Fast-WAM runtime on the IQ-9075 — from the host, one command.
3
+ #
4
+ # Copies the LATEST runtime/*.py from this repo to an EPHEMERAL workspace under the
5
+ # device's /tmp, symlinks the heavy assets (ctx/ golden/ qnn_libs/ dsp_libs/) from the
6
+ # persistent /root/fastwam_workspace (no multi-GB copy), runs resident_run.py, prints the
7
+ # latency table + per-step pure [infer-ms], then deletes the temp dir. Nothing persistent
8
+ # is modified.
9
+ #
10
+ # Usage (fp16 default; A/B a quantized action bin via env):
11
+ # runtime/profile_device.sh
12
+ # runtime/profile_device.sh ACTION_CTX_SUFFIX=_w4a16
13
+ #
14
+ # Device creds are read from IQ9_info.txt (override path with IQ9_INFO=...).
15
+ set -euo pipefail
16
+
17
+ HERE="$(cd "$(dirname "$0")" && pwd)"
18
+ INFO="${IQ9_INFO:-$HERE/../../IQ9_info.txt}"
19
+ [ -f "$INFO" ] || { echo "device info not found: $INFO (set IQ9_INFO=...)" >&2; exit 1; }
20
+ IP=$(awk -F'[: ]+' '/^IP/{print $2}' "$INFO")
21
+ PW=$(awk -F'[: ]+' '/passwd/{print $2}' "$INFO")
22
+ PERSIST=/root/fastwam_workspace
23
+ ENVS="$*"
24
+
25
+ SSH="sshpass -p $PW ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 root@$IP"
26
+ SCP="sshpass -p $PW scp -o StrictHostKeyChecking=no"
27
+
28
+ echo "[profile] device $IP envs: ${ENVS:-<fp16>}"
29
+
30
+ TMP=$($SSH "mktemp -d /tmp/fastwam_run.XXXXXX")
31
+ cleanup() { $SSH "rm -rf $TMP" >/dev/null 2>&1 || true; echo "[profile] cleaned $TMP"; }
32
+ trap cleanup EXIT
33
+ $SSH "mkdir -p $TMP/runtime $TMP/tmp && for d in ctx golden qnn_libs dsp_libs; do ln -sfn $PERSIST/\$d $TMP/\$d; done"
34
+
35
+ $SCP "$HERE"/resident_run.py "$HERE"/resident_worker.py root@"$IP":"$TMP"/runtime/ >/dev/null
36
+
37
+ $SSH "cd $TMP && FASTWAM_WS=$TMP $ENVS python3 -u runtime/resident_run.py"
38
+
39
+ echo "[profile] pure ctx.Inference times ([infer-ms], excl. TCP):"
40
+ $SSH "grep -h infer-ms $TMP/tmp/w_*.log 2>/dev/null | awk '{a[\$2]+=\$3;n[\$2]++} END{for(k in a) printf \" %-12s %.1f ms\n\", k, a[k]/n[k]}' | sort" || true
runtime/resident_run.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Launch the resident Fast-WAM runtime and run the action-expert denoise loop on the NPU
3
+ WITHOUT reload, verify parity vs the PyTorch golden, and measure latency.
4
+
5
+ Fast-WAM's on-device unit here is the MoT **action expert** (`action_step`), which cross-
6
+ attends to the video KV cache produced by the video world-model DiT (`video_prefill`).
7
+ The video prefill runs host-side for now (its context binary needs a MolmoAct2-style
8
+ layer-split, see README), so the host:
9
+ 1. loads the video KV cache + context (here: from golden/, i.e. the video_prefill output),
10
+ 2. runs the flow-matching Euler loop: for each of num_inference_steps, feed the current
11
+ latents_action + timestep -> pred_noise (NPU), then update latents with the Wan
12
+ continuous flow-match step latents += pred * delta,
13
+ 3. verifies the single-step output vs golden (parity gate), reports latency.
14
+
15
+ Pipeline (on-device portion): action_step ×N (resident). No per-inference reload.
16
+ """
17
+ import sys, os, socket, struct, subprocess, time
18
+ import numpy as np
19
+
20
+ WS = os.environ.get("FASTWAM_WS", "/root/fastwam_workspace")
21
+ RT = f"{WS}/runtime"
22
+ ROLES = ["action_step"]
23
+ PORT0 = 5801
24
+ STEPS = int(os.environ.get("FASTWAM_STEPS", "10")) # num_inference_steps
25
+ SHIFT = float(os.environ.get("FASTWAM_SHIFT", "5.0")) # action_scheduler infer_shift
26
+ NUM_TRAIN = int(os.environ.get("FASTWAM_NUM_TRAIN_TIMESTEPS", "1000"))
27
+
28
+
29
+ def recvall(s, n):
30
+ b = bytearray()
31
+ while len(b) < n:
32
+ c = s.recv(min(n - len(b), 8 << 20))
33
+ if not c:
34
+ raise ConnectionError()
35
+ b += c
36
+ return bytes(b)
37
+
38
+
39
+ def recv_tensors(s):
40
+ n = struct.unpack("<I", recvall(s, 4))[0]
41
+ out = {}
42
+ for _ in range(n):
43
+ nl = struct.unpack("<I", recvall(s, 4))[0]; name = recvall(s, nl).decode()
44
+ dl = struct.unpack("<I", recvall(s, 4))[0]; dt = recvall(s, dl).decode()
45
+ nd = struct.unpack("<I", recvall(s, 4))[0]; shape = struct.unpack(f"<{nd}I", recvall(s, nd * 4))
46
+ nb = struct.unpack("<Q", recvall(s, 8))[0]; data = recvall(s, nb)
47
+ out[name] = np.frombuffer(data, dtype=dt).reshape(shape).copy()
48
+ return out
49
+
50
+
51
+ def send_tensors(s, d):
52
+ parts = [struct.pack("<I", len(d))]
53
+ for name, a in d.items():
54
+ a = np.ascontiguousarray(a, np.float32)
55
+ nb = name.encode(); dt = str(a.dtype).encode()
56
+ parts.append(struct.pack("<I", len(nb)) + nb)
57
+ parts.append(struct.pack("<I", len(dt)) + dt)
58
+ parts.append(struct.pack("<I", a.ndim) + struct.pack(f"<{a.ndim}I", *a.shape))
59
+ parts.append(struct.pack("<Q", a.nbytes) + a.tobytes())
60
+ s.sendall(b"".join(parts))
61
+
62
+
63
+ def call(sock, d):
64
+ send_tensors(sock, d)
65
+ return recv_tensors(sock)
66
+
67
+
68
+ def cos(r, g):
69
+ r = np.asarray(r, np.float64).ravel(); g = np.asarray(g, np.float64).ravel()
70
+ return float(r @ g / (np.linalg.norm(r) * np.linalg.norm(g) + 1e-12))
71
+
72
+
73
+ def build_schedule(steps, shift, num_train):
74
+ """Wan continuous flow-match inference schedule (vendored from video_dit.py):
75
+ sigma = linspace(1,0,steps+1)[:steps]; sigma = shift*sigma/(1+(shift-1)*sigma).
76
+ timesteps = sigma*num_train; deltas = sigma_next - sigma (sigma_next[-1]=0)."""
77
+ sigma = np.linspace(1, 0, steps + 1)[:steps]
78
+ sigma = shift * sigma / (1 + (shift - 1) * sigma)
79
+ timesteps = (sigma * num_train).astype(np.float32)
80
+ sigma_next = np.concatenate([sigma[1:], np.zeros(1)])
81
+ deltas = (sigma_next - sigma).astype(np.float32)
82
+ return timesteps, deltas
83
+
84
+
85
+ def main():
86
+ os.makedirs(f"{WS}/tmp", exist_ok=True)
87
+ for r in ROLES:
88
+ try: os.remove(f"{WS}/tmp/w_{r}.ready")
89
+ except OSError: pass
90
+ env = os.environ.copy()
91
+ env["LD_LIBRARY_PATH"] = f"{WS}/qnn_libs:" + env.get("LD_LIBRARY_PATH", "")
92
+ env["ADSP_LIBRARY_PATH"] = f"{WS}/dsp_libs"
93
+
94
+ procs, ports = {}, {}
95
+ t0 = time.time()
96
+ for i, r in enumerate(ROLES):
97
+ ports[r] = PORT0 + i
98
+ logf = open(f"{WS}/tmp/w_{r}.log", "w")
99
+ procs[r] = subprocess.Popen(["python3", "-u", f"{RT}/resident_worker.py", r, str(ports[r])],
100
+ env=env, stdout=logf, stderr=subprocess.STDOUT)
101
+ print(f"[launch] {len(ROLES)} worker spawning ...", flush=True)
102
+ for r in ROLES:
103
+ for _ in range(240):
104
+ if os.path.exists(f"{WS}/tmp/w_{r}.ready"):
105
+ break
106
+ if procs[r].poll() is not None:
107
+ print(f"[ERR] worker {r} died during load (rc={procs[r].returncode}); see tmp/w_{r}.log")
108
+ return 3
109
+ time.sleep(0.5)
110
+ else:
111
+ print(f"[ERR] worker {r} not ready"); return 3
112
+ print(f"[launch] worker RESIDENT in {time.time()-t0:.1f}s (1 NPU session)", flush=True)
113
+
114
+ socks = {}
115
+ for r in ROLES:
116
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
117
+ s.connect(("127.0.0.1", ports[r])); s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
118
+ socks[r] = s
119
+
120
+ g = np.load(f"{WS}/golden/action_step_io.npz")
121
+ vk = g["video_keys"].astype(np.float32) # [30,1,98,3072] (video world-model KV cache)
122
+ vv = g["video_values"].astype(np.float32)
123
+ ctx = g["context"].astype(np.float32) # [1,129,4096]
124
+ cmask = g["context_mask"].astype(np.float32) # [1,129]
125
+ la0 = g["latents_action"].astype(np.float32) # [1,32,7] initial noise
126
+ ts0 = g["timestep"].astype(np.float32) # [1] step-0 timestep
127
+ pred_gold = g["pred"].astype(np.float32) # [1,32,7] one-step reference
128
+
129
+ const = {"video_keys": vk, "video_values": vv, "context": ctx, "context_mask": cmask}
130
+
131
+ # ---- parity: single step with golden inputs vs golden pred ----
132
+ out1 = call(socks["action_step"], {**const, "latents_action": la0, "timestep": ts0})
133
+ key = "pred_noise" if "pred_noise" in out1 else list(out1)[0]
134
+ c = cos(pred_gold, out1[key])
135
+ print(f"[verify] action_step single-step pred cos={c:.8f} (PASS={c>=0.999})", flush=True)
136
+
137
+ # ---- full N-step flow-match loop (latency) ----
138
+ timesteps, deltas = build_schedule(STEPS, SHIFT, NUM_TRAIN)
139
+
140
+ def run_loop(timing=None):
141
+ la = la0.copy()
142
+ for i in range(STEPS):
143
+ ts = np.array([timesteps[i]], np.float32)
144
+ s = time.time()
145
+ o = call(socks["action_step"], {**const, "latents_action": la, "timestep": ts})
146
+ if timing is not None:
147
+ timing["action_step"] = timing.get("action_step", 0) + time.time() - s
148
+ pred = np.ascontiguousarray(o[key], np.float32).reshape(la.shape)
149
+ la = la + pred * deltas[i] # Wan continuous flow-match step: x += pred*delta
150
+ return la
151
+
152
+ run_loop() # warmup
153
+ N = 5
154
+ timing = {}
155
+ t0 = time.time()
156
+ for _ in range(N):
157
+ run_loop(timing)
158
+ total = (time.time() - t0) / N
159
+ print(f"\n=== RESIDENT LATENCY (avg of {N}, 1 session, NO reload, {STEPS}-step flow-match) ===", flush=True)
160
+ print(f" action_step (x{STEPS}) {timing['action_step']/N*1000:7.1f} ms (incl. video-KV TCP each step)", flush=True)
161
+ print(f" {'per-step (wall)':18} {timing['action_step']/N/STEPS*1000:7.1f} ms", flush=True)
162
+ print(f" {'TOTAL':18} {total*1000:7.1f} ms", flush=True)
163
+ print(" (pure NPU ctx.Inference per step -> tmp/w_action_step.log [infer-ms])", flush=True)
164
+
165
+ for s in socks.values():
166
+ try: s.close()
167
+ except OSError: pass
168
+ time.sleep(1)
169
+ for p in procs.values():
170
+ try: p.terminate()
171
+ except OSError: pass
172
+ return 0
173
+
174
+
175
+ if __name__ == "__main__":
176
+ sys.exit(main())
runtime/resident_worker.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Persistent resident QNN worker for Fast-WAM (one NPU session per process).
3
+
4
+ Loads the `action_step` HTP context ONCE, stays alive, serves inference over a TCP
5
+ socket (no per-inference reload). Fast-WAM's on-device unit here is the MoT **action
6
+ expert** denoise step — it cross-attends to the video KV cache produced by the video
7
+ world-model DiT (`video_prefill`), which runs host-side for now (see README).
8
+
9
+ role:
10
+ action_step : one flow-matching denoise step of the action expert.
11
+ inputs : latents_action [1,32,7], timestep [1], video_keys [30,1,98,3072],
12
+ video_values [30,1,98,3072], context [1,129,4096], context_mask [1,129]
13
+ output : pred_noise [1,32,7] (host applies the Euler/flow-match update)
14
+
15
+ Usage: resident_worker.py <role> <port>
16
+ """
17
+ import sys, os, socket, struct, time
18
+ import numpy as np
19
+
20
+ WS = os.environ.get("FASTWAM_WS", "/root/fastwam_workspace")
21
+
22
+
23
+ def recvall(s, n):
24
+ b = bytearray()
25
+ while len(b) < n:
26
+ c = s.recv(min(n - len(b), 8 << 20))
27
+ if not c:
28
+ raise ConnectionError()
29
+ b += c
30
+ return bytes(b)
31
+
32
+
33
+ def recv_tensors(s):
34
+ n = struct.unpack("<I", recvall(s, 4))[0]
35
+ out = {}
36
+ for _ in range(n):
37
+ nl = struct.unpack("<I", recvall(s, 4))[0]; name = recvall(s, nl).decode()
38
+ dl = struct.unpack("<I", recvall(s, 4))[0]; dt = recvall(s, dl).decode()
39
+ nd = struct.unpack("<I", recvall(s, 4))[0]; shape = struct.unpack(f"<{nd}I", recvall(s, nd * 4))
40
+ nb = struct.unpack("<Q", recvall(s, 8))[0]; data = recvall(s, nb)
41
+ out[name] = np.frombuffer(data, dtype=dt).reshape(shape).copy()
42
+ return out
43
+
44
+
45
+ def send_tensors(s, d):
46
+ parts = [struct.pack("<I", len(d))]
47
+ for name, a in d.items():
48
+ a = np.ascontiguousarray(a)
49
+ nb = name.encode(); dt = str(a.dtype).encode()
50
+ parts.append(struct.pack("<I", len(nb)) + nb)
51
+ parts.append(struct.pack("<I", len(dt)) + dt)
52
+ parts.append(struct.pack("<I", a.ndim) + struct.pack(f"<{a.ndim}I", *a.shape))
53
+ parts.append(struct.pack("<Q", a.nbytes) + a.tobytes())
54
+ s.sendall(b"".join(parts))
55
+
56
+
57
+ def feed(ctx, m):
58
+ """Order inputs to match the context; QNN consumes float32 raws (bool mask too)."""
59
+ out = []
60
+ for nm, shp in zip(ctx.getInputName(), ctx.getInputShapes()):
61
+ a = np.asarray(m[nm], np.float32)
62
+ if tuple(a.shape) != tuple(shp) and a.size == int(np.prod(shp)):
63
+ a = a.reshape(shp)
64
+ out.append(np.ascontiguousarray(a, np.float32))
65
+ return out
66
+
67
+
68
+ def named(ctx, o):
69
+ return {nm: np.asarray(o[i], np.float32) for i, nm in enumerate(ctx.getOutputName())}
70
+
71
+
72
+ def main():
73
+ role, port = sys.argv[1], int(sys.argv[2])
74
+ from qai_appbuilder import QNNConfig, QNNContext, PerfProfile, Runtime, LogLevel
75
+ QNNConfig.Config(qnn_lib_path=f"{WS}/qnn_libs", runtime=Runtime.HTP, log_level=LogLevel.ERROR)
76
+
77
+ suffix = os.environ.get("ACTION_CTX_SUFFIX", "")
78
+ ctx = QNNContext(role, f"{WS}/ctx/{role}{suffix}_socid77_archv73.bin")
79
+ PerfProfile.SetPerfProfileGlobal(PerfProfile.BURST)
80
+
81
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
82
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
83
+ srv.bind(("127.0.0.1", port)); srv.listen(1)
84
+ open(f"{WS}/tmp/w_{role}.ready", "w").write("1")
85
+ conn, _ = srv.accept()
86
+ conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
87
+
88
+ while True:
89
+ try:
90
+ req = recv_tensors(conn)
91
+ except (ConnectionError, struct.error):
92
+ break
93
+ _t = time.time()
94
+ o = ctx.Inference(feed(ctx, req))
95
+ print(f"[infer-ms] {role} {(time.time()-_t)*1000:.1f}", flush=True)
96
+ send_tensors(conn, named(ctx, o))
97
+
98
+ os._exit(0) # dodge an appbuilder<->libs teardown double-free (see README device gotchas)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()