Download src/tool/depthart_export_axonnx.py from AXERA-TECH/DepthART: direct link, hf CLI and curl.
- Browser
- Download file 30.6 kB
-
https://huggingface.co/AXERA-TECH/DepthART/resolve/main/src/tool/depthart_export_axonnx.py
- Command line
-
hf download hf://AXERA-TECH/DepthART/src/tool/depthart_export_axonnx.py
-
curl -L -o depthart_export_axonnx.py https://huggingface.co/AXERA-TECH/DepthART/resolve/main/src/tool/depthart_export_axonnx.py
30.6 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| 把 DepthART ONNX 中的 com.depthart::SelectiveScan 改写为 pulsar2 可编译的纯 ONNX 子图。 | |
| 背景: | |
| 官方 relative_* ONNX 含自定义算子 SelectiveScan,pulsar2 前端直接报 | |
| "dont support SelectiveScan opr"。官方只提供 TensorRT 插件,没有 NPU 实现。 | |
| 推理脚本里的 Loop 改写可以跑通 onnxruntime,但 AX 工具链不支持 Loop 控制流。 | |
| 做法: | |
| 选择性扫描是线性递推 h_t = exp(dt_t*A) * h_{t-1} + dt_t*B_t*u_t,可写成 | |
| h_t = exp(L_t) * h_{-1} + sum_{s<=t} exp(L_t - L_s) * dt_s*B_s*u_s | |
| 其中 L_t = cumsum_{r<=t}(dt_r*A)。把时间轴切成固定长度 T(默认 14),每个 | |
| 分块内用 CumSum + 下三角 mask + MatMul 一次算出,块间只传一个状态。所有 | |
| 指数项的指数都 <= 0,不会溢出。所有算子(CumSum/Exp/Where/MatMul 等)都在 | |
| AX 前端支持列表内,不含 Loop/Scan/自定义算子。 | |
| 校验: | |
| - 本工具用 Identity 替换 scan 跑一遍,拿到每个 scan 的 D/G/N/L,再按实际 | |
| 尺寸生成静态图;时间轴不是 T 整数倍时自动补零并在输出处裁掉。 | |
| - 改写后的整模型输出与已验证的 Loop 版本最大绝对误差约 6e-6(FP32)。 | |
| - 同时把 4D `Einsum` 换成按 k 的批 MatMul(`b k d l, k c d -> b k c l` | |
| 和 `b k r l, k d r -> b k d l`):pulsar2 的量化执行器不支持 4D Einsum, | |
| 会报 Op Execution Error。 | |
| 用法(在 hs/DepthART 目录下执行): | |
| # 默认:448 模型,块大小 14,输出 onnx/relative_s_448_default_axonnx.onnx | |
| ../../venv/bin/python src/tool/depthart_export_axonnx.py | |
| # 指定模型/输出/块大小 | |
| ../../venv/bin/python src/tool/depthart_export_axonnx.py \\ | |
| --model onnx/relative_b_448_default.onnx \\ | |
| --output onnx/relative_b_448_default_axonnx.onnx --chunk-size 14 | |
| # 改写后用推理脚本验证数值 | |
| ../../venv/bin/python src/infer/depthart_infer_onnx.py \\ | |
| --model onnx/relative_s_448_default_axonnx.onnx --image /path/a.jpg | |
| 注意: | |
| - 块内两两指数矩阵形状为 [D*N, T, T],T 越大显存/内存占用越大; | |
| T=14 时 672 通道层约 1M 元素(4 MB),推荐保持默认。 | |
| - 输出是标准 ONNX,不再含自定义算子;原模型文件不会被修改。 | |
| """ | |
| import argparse | |
| import logging | |
| from pathlib import Path | |
| import numpy as np | |
| import onnx | |
| import onnxruntime as ort | |
| from onnx import helper, numpy_helper, TensorProto | |
| PROJECT_ROOT = Path(__file__).resolve().parents[2] | |
| DEFAULT_MODEL = PROJECT_ROOT / 'onnx' / 'relative_s_448_default.onnx' | |
| DOMAIN = 'com.depthart' | |
| OP = 'SelectiveScan' | |
| EINSUM_EQUATIONS = { | |
| 'b k d l, k c d -> b k c l', | |
| 'b k r l, k d r -> b k d l', | |
| } | |
| def collect_runtime_shapes(model): | |
| """用 ORT 实际跑一遍,读取 scan 与 Einsum 的静态形状。 | |
| scan 会被替换成 Identity(输出形状等于 u,不影响下游形状);Einsum 正常执行。 | |
| 返回 (scan_shapes, einsum_shapes): | |
| scan_shapes : 按图顺序的 [(D, G, N, L), ...] | |
| einsum_shapes: {节点名: (x_shape, y_shape)} | |
| """ | |
| nodes = [] | |
| scan_outputs, b_aliases = [], {} | |
| einsum_x, einsum_y = {}, {} | |
| for index, node in enumerate(model.graph.node): | |
| if node.domain == DOMAIN and node.op_type == OP: | |
| nodes.append(helper.make_node('Identity', [node.input[0]], [node.output[0]], | |
| name=f'depthart_stub_scan_{index}')) | |
| alias = f'depthart_stub_b_{index}' | |
| nodes.append(helper.make_node('Identity', [node.input[3]], [alias], | |
| name=f'depthart_stub_bnode_{index}')) | |
| scan_outputs.append(node.output[0]) | |
| b_aliases[node.output[0]] = alias | |
| else: | |
| if node.op_type == 'Einsum': | |
| x_alias = f'depthart_stub_einsum_x_{index}' | |
| y_alias = f'depthart_stub_einsum_y_{index}' | |
| nodes.append(helper.make_node('Identity', [node.input[0]], [x_alias], | |
| name=f'{x_alias}_node')) | |
| nodes.append(helper.make_node('Identity', [node.output[0]], [y_alias], | |
| name=f'{y_alias}_node')) | |
| einsum_x[node.name] = x_alias | |
| einsum_y[node.name] = y_alias | |
| nodes.append(node) | |
| stub = onnx.ModelProto() | |
| stub.CopyFrom(model) | |
| del stub.graph.node[:] | |
| stub.graph.node.extend(nodes) | |
| for name in scan_outputs: | |
| stub.graph.output.append(helper.make_tensor_value_info(name, TensorProto.FLOAT, None)) | |
| stub.graph.output.append(helper.make_tensor_value_info(b_aliases[name], | |
| TensorProto.FLOAT, None)) | |
| for x_alias, y_alias in zip(einsum_x.values(), einsum_y.values()): | |
| stub.graph.output.append(helper.make_tensor_value_info(x_alias, TensorProto.FLOAT, None)) | |
| stub.graph.output.append(helper.make_tensor_value_info(y_alias, TensorProto.FLOAT, None)) | |
| meta = model.graph.input[0] | |
| shape = [dim.dim_value for dim in meta.type.tensor_type.shape.dim] | |
| if not all(shape): | |
| raise ValueError(f'输入必须是静态形状: {meta.name} {shape}') | |
| session = ort.InferenceSession(stub.SerializeToString()) | |
| values = dict(zip([out.name for out in session.get_outputs()], | |
| session.run(None, {meta.name: np.zeros(shape, dtype=np.float32)}))) | |
| scan_map = {} | |
| for name in scan_outputs: | |
| u_shape = values[name].shape | |
| b_shape = values[b_aliases[name]].shape | |
| if len(u_shape) != 3 or len(b_shape) != 4: | |
| raise ValueError(f'scan 形状不符合预期: u={u_shape} B={b_shape}') | |
| scan_map[name] = (int(u_shape[1]), int(b_shape[1]), int(b_shape[2]), int(u_shape[2])) | |
| scan_shapes = [scan_map[node.output[0]] for node in model.graph.node | |
| if node.domain == DOMAIN and node.op_type == OP] | |
| einsum_shapes = {name: (values[einsum_x[name]].shape, values[einsum_y[name]].shape) | |
| for name in einsum_x} | |
| return scan_shapes, einsum_shapes | |
| class Rewriter: | |
| def __init__(self, model, chunk_size, stub_scan=False, scan_method='pairwise'): | |
| self.model = model | |
| self.chunk_size = chunk_size | |
| self.stub_scan = stub_scan | |
| self.scan_method = scan_method | |
| self.initializers = list(model.graph.initializer) | |
| self.names = {tensor.name for tensor in self.initializers} | |
| self.initializer_shapes = {tensor.name: tuple(tensor.dims) | |
| for tensor in model.graph.initializer} | |
| self.shared = {} | |
| def build_einsum(self, prefix, node, x_shape, y_shape): | |
| """b k d l, k c d -> b k c l (以及 b k r l, k d r -> b k d l)。 | |
| 两条式子都是按 k 的批 MatMul:W[k,out,in] @ x[b=1,k,in,l]。 | |
| 替换掉 4D Einsum,避免量化执行器不支持。 | |
| """ | |
| out_name = node.output[0] | |
| w_shape = self.initializer_shapes.get(node.input[1]) | |
| if w_shape is None: | |
| raise ValueError(f'{node.name}: 第二个输入不是常量权重,暂不支持') | |
| if len(w_shape) != 3 or len(x_shape) != 4: | |
| raise ValueError(f'{node.name}: Einsum 形状不符合预期: {w_shape} {x_shape}') | |
| if int(x_shape[0]) != 1: | |
| raise ValueError(f'{node.name}: 仅支持 batch=1,实际 {x_shape}') | |
| K, out_dim, in_dim = (int(v) for v in w_shape) | |
| length = int(x_shape[3]) | |
| if int(x_shape[1]) != K: | |
| raise ValueError(f'{node.name}: K 不一致: {x_shape} vs {w_shape}') | |
| expected = (1, K, out_dim, length) | |
| if tuple(int(v) for v in y_shape) != expected: | |
| raise ValueError(f'{node.name}: 输出形状 {y_shape} != {expected}') | |
| nodes = [] | |
| # 用 Squeeze/Unsqueeze 而不是 Reshape 还原 batch 维:onnxslim 会把 | |
| # MatMul 前后的 Reshape 当成冗余节点合并/改名,导致下游张量断链。 | |
| nodes.append(helper.make_node( | |
| 'Squeeze', [node.input[0], self.shared_const('axes0', np.array([0], np.int64))], | |
| [f'{prefix}_x2'])) | |
| nodes.append(helper.make_node('MatMul', [node.input[1], f'{prefix}_x2'], | |
| [f'{prefix}_y3'])) | |
| nodes.append(helper.make_node( | |
| 'Unsqueeze', [f'{prefix}_y3', self.shared_const('axes0', np.array([0], np.int64))], | |
| [out_name])) | |
| return nodes | |
| def const(self, name, value): | |
| if name not in self.names: | |
| self.initializers.append(numpy_helper.from_array(np.asarray(value), name)) | |
| self.names.add(name) | |
| return name | |
| def shared_const(self, key, value): | |
| if key not in self.shared: | |
| self.shared[key] = self.const(f'depthart_{key}', value) | |
| return self.shared[key] | |
| def build_logspace(self, prefix, shapes, inputs): | |
| """对数空间分块递推:每块 O(T) 而非 O(T^2),不构造 [D*N,T,T] 两两矩阵。 | |
| h_t = exp(L_t) * ( h_prev + cumsum_s( exp(-L_s) * dt_s * B_s * u_s ) ) | |
| 其中 L 为块内相对累计衰减(L_0=0,L_t<=0)。要求块内 exp(-L) 不超 U16 量程, | |
| 因此 T 要小(T=4 时最大约 e^9≈8e3)。 | |
| """ | |
| D, G, N, L = shapes | |
| u, delta, a, b, c = inputs[:5] | |
| d = inputs[5] if len(inputs) > 5 and inputs[5] else None | |
| bias = inputs[6] if len(inputs) > 6 and inputs[6] else None | |
| attrs = {item.name: item.i for item in self.node_attrs} | |
| T = self.chunk_size | |
| nodes = [] | |
| dt = delta | |
| if bias is not None: | |
| bias3 = self.const(f'{prefix}_bias3_shape', np.array([1, D, 1], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [bias, bias3], [f'{prefix}_bias3'])) | |
| nodes.append(helper.make_node('Add', [delta, f'{prefix}_bias3'], [f'{prefix}_dtb'])) | |
| dt = f'{prefix}_dtb' | |
| if int(attrs.get('delta_softplus', 0)): | |
| nodes.append(helper.make_node('Softplus', [dt], [f'{prefix}_dt'])) | |
| dt = f'{prefix}_dt' | |
| # 补零到 T 的整数倍,避免最后一块长度不足 | |
| pad = (-L) % T | |
| Lp = L + pad | |
| if pad: | |
| nodes.append(helper.make_node( | |
| 'Concat', [dt, self.const(f'{prefix}_pad_dt', np.zeros((1, D, pad), np.float32))], | |
| [f'{prefix}_dt_pad'], axis=2)) | |
| nodes.append(helper.make_node( | |
| 'Concat', [u, self.const(f'{prefix}_pad_u', np.zeros((1, D, pad), np.float32))], | |
| [f'{prefix}_u_pad'], axis=2)) | |
| pad_bc = self.const(f'{prefix}_pad_bc', np.zeros((1, G, N, pad), np.float32)) | |
| nodes.append(helper.make_node('Concat', [b, pad_bc], [f'{prefix}_b_pad'], axis=3)) | |
| nodes.append(helper.make_node('Concat', [c, pad_bc], [f'{prefix}_c_pad'], axis=3)) | |
| dt, u, b, c = (f'{prefix}_dt_pad', f'{prefix}_u_pad', | |
| f'{prefix}_b_pad', f'{prefix}_c_pad') | |
| # dtA = dt * A -> [1,D,N,Lp] | |
| a4 = self.const(f'{prefix}_a4_shape', np.array([1, D, N, 1], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [a, a4], [f'{prefix}_a4'])) | |
| dt3 = self.const(f'{prefix}_dt3_shape', np.array([1, D, 1, Lp], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [dt, dt3], [f'{prefix}_dt4'])) | |
| nodes.append(helper.make_node('Mul', [f'{prefix}_dt4', f'{prefix}_a4'], | |
| [f'{prefix}_dtA'])) | |
| b_full = self.expand_groups(b, D, G, N, Lp, f'{prefix}_b', nodes) | |
| c_full = self.expand_groups(c, D, G, N, Lp, f'{prefix}_c', nodes) | |
| if d is not None: | |
| nodes.append(helper.make_node('Reshape', [d, | |
| self.const(f'{prefix}_d_shape', | |
| np.array([1, D, 1], np.int64))], | |
| [f'{prefix}_d3'])) | |
| d = f'{prefix}_d3' | |
| axes_u2 = self.shared_const('axes_u2', np.array([2], np.int64)) | |
| axes_rs = self.shared_const('axes_rs2', np.array([2], np.int64)) | |
| axis_t = self.shared_const('axis2', np.array([2], np.int64)) | |
| axis_bc = self.shared_const('axis3', np.array([3], np.int64)) | |
| h_prev = self.const(f'{prefix}_h0', np.zeros((1, D, N, 1), np.float32)) | |
| outputs = [] | |
| for k in range(Lp // T): | |
| start, end = k * T, (k + 1) * T | |
| tag = f'{prefix}_c{k}' | |
| starts = self.const(f'{tag}_starts', np.array([start], np.int64)) | |
| ends = self.const(f'{tag}_ends', np.array([end], np.int64)) | |
| for source, suffix, axis in ((f'{prefix}_dtA', 'dta', axis_bc), | |
| (dt, 'dt', axis_t), (u, 'u', axis_t), | |
| (b_full, 'b', axis_bc), (c_full, 'c', axis_bc)): | |
| nodes.append(helper.make_node('Slice', [source, starts, ends, axis], | |
| [f'{tag}_{suffix}'])) | |
| # L = cumsum(dtA)(块内,含首步);h = exp(L) * (h_prev + cumsum(exp(-L)*b)) | |
| nodes.append(helper.make_node('CumSum', [f'{tag}_dta', | |
| self.shared_const('axis3s', np.array(3, np.int64))], | |
| [f'{tag}_L'])) | |
| nodes.append(helper.make_node('Exp', [f'{tag}_L'], [f'{tag}_eL'])) | |
| nodes.append(helper.make_node('Neg', [f'{tag}_L'], [f'{tag}_negL'])) | |
| nodes.append(helper.make_node('Exp', [f'{tag}_negL'], [f'{tag}_eneg'])) | |
| # b = dt * u * B;G = cumsum(exp(-Lp) * b) | |
| nodes.append(helper.make_node('Unsqueeze', [f'{tag}_dt', axes_u2], [f'{tag}_dt3'])) | |
| nodes.append(helper.make_node('Unsqueeze', [f'{tag}_u', axes_u2], [f'{tag}_u3'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_dt3', f'{tag}_u3'], [f'{tag}_dtu'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_dtu', f'{tag}_b'], [f'{tag}_bt'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_eneg', f'{tag}_bt'], [f'{tag}_g'])) | |
| nodes.append(helper.make_node('CumSum', [f'{tag}_g', | |
| self.shared_const('axis3s', np.array(3, np.int64))], | |
| [f'{tag}_G'])) | |
| nodes.append(helper.make_node('Add', [f'{tag}_G', h_prev], [f'{tag}_hp'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_eL', f'{tag}_hp'], [f'{tag}_h'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_h', f'{tag}_c'], [f'{tag}_hc'])) | |
| nodes.append(helper.make_node('ReduceSum', [f'{tag}_hc', axes_rs], | |
| [f'{tag}_ysum'], keepdims=0)) | |
| if d is not None: | |
| nodes.append(helper.make_node('Mul', [d, f'{tag}_u'], [f'{tag}_du'])) | |
| nodes.append(helper.make_node('Add', [f'{tag}_ysum', f'{tag}_du'], | |
| [f'{tag}_y'])) | |
| else: | |
| nodes.append(helper.make_node('Identity', [f'{tag}_ysum'], [f'{tag}_y'])) | |
| outputs.append(f'{tag}_y') | |
| nodes.append(helper.make_node('Slice', [f'{tag}_h', | |
| self.const(f'{tag}_hl', np.array([T - 1], np.int64)), | |
| self.const(f'{tag}_he', np.array([T], np.int64)), | |
| axis_bc], [f'{tag}_hnext'])) | |
| h_prev = f'{tag}_hnext' | |
| nodes.append(helper.make_node('Concat', outputs, [f'{prefix}_ycat'], axis=2)) | |
| if pad: | |
| nodes.append(helper.make_node('Slice', [ | |
| f'{prefix}_ycat', | |
| self.const(f'{prefix}_out_start', np.array([0], np.int64)), | |
| self.const(f'{prefix}_out_end', np.array([L], np.int64)), | |
| self.shared_const('axis2', np.array([2], np.int64))], [self.out_name])) | |
| else: | |
| nodes.append(helper.make_node('Identity', [f'{prefix}_ycat'], [self.out_name])) | |
| return nodes | |
| def build(self, prefix, shapes, inputs): | |
| D, G, N, L = shapes | |
| u, delta, a, b, c = inputs[:5] | |
| d = inputs[5] if len(inputs) > 5 and inputs[5] else None | |
| bias = inputs[6] if len(inputs) > 6 and inputs[6] else None | |
| attrs = {item.name: item.i for item in self.node_attrs} | |
| T = self.chunk_size | |
| nodes = [] | |
| # dt = softplus(delta + bias) | |
| dt = delta | |
| if bias is not None: | |
| bias3 = self.const(f'{prefix}_bias3_shape', np.array([1, D, 1], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [bias, bias3], [f'{prefix}_bias3'])) | |
| nodes.append(helper.make_node('Add', [delta, f'{prefix}_bias3'], [f'{prefix}_dtb'])) | |
| dt = f'{prefix}_dtb' | |
| if int(attrs.get('delta_softplus', 0)): | |
| nodes.append(helper.make_node('Softplus', [dt], [f'{prefix}_dt'])) | |
| dt = f'{prefix}_dt' | |
| # 时间轴补零到 T 的整数倍 | |
| pad = (-L) % T | |
| Lp = L + pad | |
| if pad: | |
| nodes.append(helper.make_node( | |
| 'Concat', [dt, self.const(f'{prefix}_pad_dt', np.zeros((1, D, pad), np.float32))], | |
| [f'{prefix}_dt_pad'], axis=2)) | |
| nodes.append(helper.make_node( | |
| 'Concat', [u, self.const(f'{prefix}_pad_u', np.zeros((1, D, pad), np.float32))], | |
| [f'{prefix}_u_pad'], axis=2)) | |
| pad_bc = self.const(f'{prefix}_pad_bc', np.zeros((1, G, N, pad), np.float32)) | |
| nodes.append(helper.make_node('Concat', [b, pad_bc], [f'{prefix}_b_pad'], axis=3)) | |
| nodes.append(helper.make_node('Concat', [c, pad_bc], [f'{prefix}_c_pad'], axis=3)) | |
| dt, u, b, c = (f'{prefix}_dt_pad', f'{prefix}_u_pad', | |
| f'{prefix}_b_pad', f'{prefix}_c_pad') | |
| # B/C 按组连续复制:[1,G,N,Lp] -> [1,D,N,Lp] | |
| b_full = self.expand_groups(b, D, G, N, Lp, f'{prefix}_b', nodes) | |
| c_full = self.expand_groups(c, D, G, N, Lp, f'{prefix}_c', nodes) | |
| # A -> [1,D,N,1],dt -> [1,D,N,T],再摊平成 [D*N,T] 做 CumSum | |
| a4 = self.const(f'{prefix}_a_shape', np.array([1, D, N, 1], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [a, a4], [f'{prefix}_a4'])) | |
| if d is not None: | |
| d3 = self.const(f'{prefix}_d_shape', np.array([1, D, 1], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [d, d3], [f'{prefix}_d3'])) | |
| d = f'{prefix}_d3' | |
| mask = np.tril(np.ones((T, T), dtype=bool)).reshape(1, T, T) | |
| mask_name = self.shared_const(f'mask_{T}', mask) | |
| neg_name = self.shared_const('mask_fill', np.array(-1e4, np.float32)) | |
| flat_axis = self.shared_const('axis1', np.array(1, np.int64)) | |
| axes_u2 = self.shared_const('axes_u2', np.array([2], np.int64)) | |
| axes_u3 = self.shared_const('axes_u3', np.array([3], np.int64)) | |
| axes_rs = self.shared_const('axes_rs2', np.array([2], np.int64)) | |
| h_prev = self.const(f'{prefix}_h0', np.zeros((1, D, N, 1), np.float32)) | |
| outputs = [] | |
| for k in range(Lp // T): | |
| start, end = k * T, (k + 1) * T | |
| tag = f'{prefix}_c{k}' | |
| starts = self.const(f'{tag}_starts', np.array([start], np.int64)) | |
| ends = self.const(f'{tag}_ends', np.array([end], np.int64)) | |
| axis_t = self.shared_const('axis2', np.array([2], np.int64)) | |
| axis_bc = self.shared_const('axis3', np.array([3], np.int64)) | |
| for source, suffix, axis in ((dt, 'dt', axis_t), (u, 'u', axis_t), | |
| (b_full, 'b', axis_bc), (c_full, 'c', axis_bc)): | |
| nodes.append(helper.make_node('Slice', [source, starts, ends, axis], | |
| [f'{tag}_{suffix}'])) | |
| # dtA = dt * A -> [1,D,N,T] -> [D*N,T],L = cumsum_t(dtA) | |
| nodes.append(helper.make_node('Unsqueeze', [f'{tag}_dt', axes_u2], [f'{tag}_dt3'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_dt3', f'{prefix}_a4'], [f'{tag}_dtA'])) | |
| nodes.append(helper.make_node('Reshape', [f'{tag}_dtA', | |
| self.const(f'{tag}_dtA2_shape', | |
| np.array([D * N, T], np.int64))], | |
| [f'{tag}_dtA2'])) | |
| nodes.append(helper.make_node('CumSum', [f'{tag}_dtA2', flat_axis], [f'{tag}_L2'])) | |
| nodes.append(helper.make_node('Reshape', [f'{tag}_L2', | |
| self.const(f'{tag}_L_shape', | |
| np.array([1, D, N, T], np.int64))], | |
| [f'{tag}_L'])) | |
| # 两两指数矩阵 E[t,s] = exp(L_t - L_s)(s>t 置为 -1e4 后 exp 为 0) | |
| nodes.append(helper.make_node('Reshape', [f'{tag}_L2', | |
| self.const(f'{tag}_Lt_shape', | |
| np.array([D * N, T, 1], np.int64))], | |
| [f'{tag}_Lt'])) | |
| nodes.append(helper.make_node('Reshape', [f'{tag}_L2', | |
| self.const(f'{tag}_Ls_shape', | |
| np.array([D * N, 1, T], np.int64))], | |
| [f'{tag}_Ls'])) | |
| nodes.append(helper.make_node('Sub', [f'{tag}_Lt', f'{tag}_Ls'], [f'{tag}_diff'])) | |
| nodes.append(helper.make_node('Where', [mask_name, f'{tag}_diff', neg_name], | |
| [f'{tag}_diffm'])) | |
| nodes.append(helper.make_node('Exp', [f'{tag}_diffm'], [f'{tag}_E'])) | |
| # b = dt * u * B,形状 [D*N,1,T];contrib[t] = sum_{s<=t} E[t,s]*b[s] | |
| nodes.append(helper.make_node('Unsqueeze', [f'{tag}_u', axes_u2], [f'{tag}_u3'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_dt3', f'{tag}_u3'], [f'{tag}_dtu'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_dtu', f'{tag}_b'], [f'{tag}_bt'])) | |
| nodes.append(helper.make_node('Reshape', [f'{tag}_bt', | |
| self.const(f'{tag}_bt2_shape', | |
| np.array([D * N, 1, T], np.int64))], | |
| [f'{tag}_bt2'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_E', f'{tag}_bt2'], [f'{tag}_eb'])) | |
| nodes.append(helper.make_node('ReduceSum', [f'{tag}_eb', | |
| self.shared_const('axis2s', np.array([2], np.int64))], | |
| [f'{tag}_contrib2'], keepdims=0)) | |
| nodes.append(helper.make_node('Reshape', [f'{tag}_contrib2', | |
| self.const(f'{tag}_contrib_shape', | |
| np.array([1, D, N, T], np.int64))], | |
| [f'{tag}_contrib'])) | |
| # 块间状态:h_t = exp(L_t) * h_prev + contrib | |
| nodes.append(helper.make_node('Exp', [f'{tag}_L'], [f'{tag}_expL'])) | |
| nodes.append(helper.make_node('Mul', [f'{tag}_expL', h_prev], [f'{tag}_carry'])) | |
| nodes.append(helper.make_node('Add', [f'{tag}_carry', f'{tag}_contrib'], | |
| [f'{tag}_h'])) | |
| # y = sum_n(h*C) + D*u | |
| nodes.append(helper.make_node('Mul', [f'{tag}_h', f'{tag}_c'], [f'{tag}_hc'])) | |
| nodes.append(helper.make_node('ReduceSum', [f'{tag}_hc', axes_rs], | |
| [f'{tag}_ysum'], keepdims=0)) | |
| if d is not None: | |
| nodes.append(helper.make_node('Mul', [d, f'{tag}_u'], [f'{tag}_du'])) | |
| nodes.append(helper.make_node('Add', [f'{tag}_ysum', f'{tag}_du'], | |
| [f'{tag}_y'])) | |
| else: | |
| nodes.append(helper.make_node('Identity', [f'{tag}_ysum'], [f'{tag}_y'])) | |
| outputs.append(f'{tag}_y') | |
| nodes.append(helper.make_node('Gather', [f'{tag}_h', | |
| self.const(f'{tag}_last', np.array(T - 1, np.int64))], | |
| [f'{tag}_hlast'], axis=3)) | |
| nodes.append(helper.make_node('Unsqueeze', [f'{tag}_hlast', axes_u3], | |
| [f'{tag}_hnext'])) | |
| h_prev = f'{tag}_hnext' | |
| nodes.append(helper.make_node('Concat', outputs, [f'{prefix}_ycat'], axis=2)) | |
| if pad: | |
| nodes.append(helper.make_node('Slice', [ | |
| f'{prefix}_ycat', | |
| self.const(f'{prefix}_out_start', np.array([0], np.int64)), | |
| self.const(f'{prefix}_out_end', np.array([L], np.int64)), | |
| self.shared_const('axis2', np.array([2], np.int64))], [self.out_name])) | |
| else: | |
| nodes.append(helper.make_node('Identity', [f'{prefix}_ycat'], [self.out_name])) | |
| return nodes | |
| def expand_groups(self, x, D, G, N, L, prefix, nodes): | |
| gs = D // G | |
| r1_shape = self.const(f'{prefix}_r1_shape', np.array([1, G, 1, N, L], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [x, r1_shape], [f'{prefix}_r1'])) | |
| target = self.const(f'{prefix}_tgt', np.array([1, G, gs, N, L], np.int64)) | |
| nodes.append(helper.make_node('Expand', [f'{prefix}_r1', target], [f'{prefix}_e'])) | |
| r2_shape = self.const(f'{prefix}_r2_shape', np.array([1, D, N, L], np.int64)) | |
| nodes.append(helper.make_node('Reshape', [f'{prefix}_e', r2_shape], [f'{prefix}_bc4'])) | |
| return f'{prefix}_bc4' | |
| def rewrite(self): | |
| shapes, einsum_shapes = collect_runtime_shapes(self.model) | |
| if not shapes: | |
| raise ValueError('模型中没有 SelectiveScan 节点,无需改写') | |
| new_nodes, index, einsum_index = [], 0, 0 | |
| for node in self.model.graph.node: | |
| if node.domain == DOMAIN and node.op_type == OP: | |
| if self.stub_scan: | |
| # 仅用于测 NPU 耗时的对照模型:扫描直接透传 u,精度无意义。 | |
| stub_node = helper.make_node('Identity', [node.input[0]], [node.output[0]], | |
| name=f'depthart_stub_scan_{index}') | |
| new_nodes.append(stub_node) | |
| index += 1 | |
| continue | |
| self.node_attrs = node.attribute | |
| self.out_name = node.output[0] | |
| builder = self.build_logspace if self.scan_method == 'log' else self.build | |
| nodes = builder(f'depthart_pscan_{index}', shapes[index], node.input) | |
| new_nodes.extend(nodes) | |
| index += 1 | |
| elif node.op_type == 'Einsum': | |
| equation = None | |
| for attr in node.attribute: | |
| if attr.name == 'equation': | |
| equation = attr.s.decode() | |
| if equation not in EINSUM_EQUATIONS: | |
| raise ValueError(f'{node.name}: 不支持的 Einsum 方程 {equation!r}') | |
| x_shape, y_shape = einsum_shapes[node.name] | |
| nodes = self.build_einsum(f'depthart_einsum_{einsum_index}', | |
| node, x_shape, y_shape) | |
| new_nodes.extend(nodes) | |
| einsum_index += 1 | |
| else: | |
| new_nodes.append(node) | |
| out = onnx.ModelProto() | |
| out.CopyFrom(self.model) | |
| del out.graph.node[:] | |
| for position, node in enumerate(new_nodes): | |
| if not node.name: | |
| node.name = f'depthart_gen_{position}' | |
| out.graph.node.extend(new_nodes) | |
| out.graph.initializer.extend(self.initializers[len(self.model.graph.initializer):]) | |
| return out | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='把 DepthART SelectiveScan 改写为 pulsar2 可编译的纯 ONNX 子图') | |
| parser.add_argument('--model', type=Path, default=DEFAULT_MODEL, | |
| help=f'输入 ONNX,默认 {DEFAULT_MODEL.relative_to(PROJECT_ROOT)}') | |
| parser.add_argument('--output', type=Path, | |
| default=PROJECT_ROOT / 'onnx' / 'relative_s_448_default_axonnx.onnx', | |
| help='输出 ONNX(不会覆盖输入模型)') | |
| parser.add_argument('--chunk-size', type=int, default=14, | |
| help='分块长度 T,默认 14;越大显存占用越高') | |
| parser.add_argument('--stub-scan', action='store_true', | |
| help='把 SelectiveScan 替换为 Identity,生成仅用于测 NPU 耗时的对照模型(精度无意义)') | |
| parser.add_argument('--scan-method', choices=('pairwise', 'log'), default='pairwise', | |
| help='pairwise=两两指数矩阵(默认);log=对数空间递推,无 T^2 矩阵,T 需小') | |
| args = parser.parse_args() | |
| logging.basicConfig(level=logging.INFO, format='%(message)s') | |
| if args.chunk_size < 1: | |
| parser.error('--chunk-size 必须为正数') | |
| if not args.model.is_file(): | |
| parser.error(f'--model 不存在: {args.model}') | |
| if args.output.resolve() == args.model.resolve(): | |
| parser.error('--output 不能与 --model 相同') | |
| model = onnx.load(str(args.model)) | |
| if not any(node.domain == DOMAIN and node.op_type == OP for node in model.graph.node): | |
| raise ValueError(f'{args.model} 中没有 {DOMAIN}::{OP} 节点') | |
| rewriter = Rewriter(model, args.chunk_size, args.stub_scan, args.scan_method) | |
| rewritten = rewriter.rewrite() | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| onnx.save(rewritten, str(args.output)) | |
| logging.info('已写出标准 ONNX: %s(节点数 %d)', args.output, len(rewritten.graph.node)) | |
| logging.info('下一步:把 pulsar2 配置的 input 指向该文件;' | |
| '可先用 src/infer/depthart_infer_onnx.py 验证数值。') | |
| if __name__ == '__main__': | |
| main() | |