| |
| """ |
| Step 1: make the exported vision ONNX runnable by ONNX Runtime / plain trtexec. |
| |
| The TensorRT-Edge-LLM export contains 32 `trt::ViTAttentionPlugin` nodes (custom TRT plugin, no ORT kernel). |
| ModelOpt ONNX PTQ calibrates with ONNX Runtime, so every plugin node is replaced by an equivalent |
| standard-ONNX attention sub-graph. Nothing else in the graph is touched (weights, FP16/FP32 mix, I/O |
| names, dtypes and shapes stay identical), so the result is still a drop-in vision encoder. |
| |
| Plugin semantics (cpp/plugins/vitAttentionPlugin): q,k,v [S,H,D] fp16, cu_seqlens int32 [B+1], |
| ragged (block-diagonal) non-causal attention with scale 1/sqrt(D), output [S,H,D] fp16. |
| |
| Replacement: seg_id[i] = #{j : i >= cu_seqlens[j]} -> mask[i,j] = 0 if seg_id[i]==seg_id[j] else -1e4 |
| out = Softmax((q*scale) @ k^T + mask) @ v (per head) |
| |
| Also: initializers that were de-duplicated by the exporter and re-used through `Identity` nodes are |
| copied back so that every Gemm bias is a real constant (ModelOpt/ORT quantizer wants constant bias). |
| """ |
| import argparse, math, os |
| from collections import Counter |
| import numpy as np |
| import onnx |
| import onnx_graphsurgeon as gs |
| from onnx import TensorProto, helper, numpy_helper |
|
|
| ROOT = os.environ.get("ROOT", "/data/users/logesh/Infernece_vision_Manual") |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--src", default=os.environ.get("SRC_ONNX", "/data/users/logesh/TensorRT-Edge-LLM/Qwen/Qwen3-VL-2B-Instruct/onnx/visual/model.onnx")) |
| ap.add_argument("--dst", default=f"{ROOT}/onnx/fp16_noplugin/model.onnx") |
| ap.add_argument("--mask_value", type=float, default=-1e4) |
| ap.add_argument("--attn_fp32", action="store_true", help="mask-add + softmax in FP32 (HF eager style); default keeps the plugin's FP16") |
| args = ap.parse_args() |
|
|
| m = onnx.load(args.src, load_external_data=True) |
| g = m.graph |
| plugin_nodes = [n for n in g.node if n.op_type == "ViTAttentionPlugin"] |
| print(f"Found {len(plugin_nodes)} ViTAttentionPlugin nodes") |
|
|
| |
| inits = {i.name: i for i in g.initializer} |
| ident = [n for n in g.node if n.op_type == "Identity" and n.input[0] in inits] |
| if ident: |
| alias = {n.output[0]: n.input[0] for n in ident} |
| new_inits = [] |
| for out_name, src_name in alias.items(): |
| t = onnx.TensorProto(); t.CopyFrom(inits[src_name]); t.name = out_name |
| new_inits.append(t) |
| g.initializer.extend(new_inits) |
| keep = [n for n in g.node if n not in ident] |
| del g.node[:]; g.node.extend(keep) |
| print(f"Un-aliased {len(ident)} Identity(initializer) nodes: {list(alias.items())}") |
|
|
| |
| new_nodes, consts = [], [] |
| def const(name, arr): |
| consts.append(numpy_helper.from_array(arr, name)); return name |
|
|
| MDT = np.float32 if args.attn_fp32 else np.float16 |
| c_zero_i64 = const("vitattn/zero_i64", np.array(0, dtype=np.int64)) |
| c_one_i64 = const("vitattn/one_i64", np.array(1, dtype=np.int64)) |
| c_axes0 = const("vitattn/axes0", np.array([0], dtype=np.int64)) |
| c_axes1 = const("vitattn/axes1", np.array([1], dtype=np.int64)) |
| c_mask0 = const("vitattn/mask_zero", np.array(0.0, dtype=MDT)) |
| c_maskneg = const("vitattn/mask_neg", np.array(args.mask_value, dtype=MDT)) |
|
|
| mask_cache = {} |
| def build_mask(cu_name, q_name): |
| if cu_name in mask_cache: |
| return mask_cache[cu_name] |
| p = f"vitattn/mask[{cu_name}]/" |
| nodes = [ |
| helper.make_node("Shape", [q_name], [p + "S1"], start=0, end=1), |
| helper.make_node("Squeeze", [p + "S1", c_axes0], [p + "S"]), |
| helper.make_node("Range", [c_zero_i64, p + "S", c_one_i64], [p + "pos"]), |
| helper.make_node("Cast", [cu_name], [p + "cu64"], to=TensorProto.INT64), |
| helper.make_node("Unsqueeze", [p + "pos", c_axes1], [p + "pos_col"]), |
| helper.make_node("Unsqueeze", [p + "cu64", c_axes0], [p + "cu_row"]), |
| helper.make_node("GreaterOrEqual", [p + "pos_col", p + "cu_row"], [p + "ge"]), |
| helper.make_node("Cast", [p + "ge"], [p + "ge_i32"], to=TensorProto.INT32), |
| helper.make_node("ReduceSum", [p + "ge_i32", c_axes1], [p + "seg"], keepdims=0), |
| helper.make_node("Unsqueeze", [p + "seg", c_axes1], [p + "seg_col"]), |
| helper.make_node("Unsqueeze", [p + "seg", c_axes0], [p + "seg_row"]), |
| helper.make_node("Equal", [p + "seg_col", p + "seg_row"], [p + "same"]), |
| helper.make_node("Where", [p + "same", c_mask0, c_maskneg], [p + "mask"]), |
| ] |
| for n in nodes: |
| n.name = n.output[0] |
| new_nodes.extend(nodes) |
| mask_cache[cu_name] = p + "mask" |
| return p + "mask" |
|
|
| replaced, out_nodes = 0, [] |
| for n in g.node: |
| if n.op_type != "ViTAttentionPlugin": |
| out_nodes.append(n); continue |
| attrs = {a.name: a.i for a in n.attribute} |
| H, D = attrs["num_heads"], attrs["head_size"] |
| q, k, v, cu, _carrier = n.input |
| out = n.output[0] |
| p = n.name + "/" |
| scale_name = const(p + "scale", np.array(1.0 / math.sqrt(D), dtype=np.float16)) |
| mask = build_mask(cu, q) |
| sub = [ |
| helper.make_node("Mul", [q, scale_name], [p + "q_scaled"]), |
| helper.make_node("Transpose", [p + "q_scaled"], [p + "qT"], perm=[1, 0, 2]), |
| helper.make_node("Transpose", [k], [p + "kT"], perm=[1, 2, 0]), |
| helper.make_node("Transpose", [v], [p + "vT"], perm=[1, 0, 2]), |
| helper.make_node("MatMul", [p + "qT", p + "kT"], [p + "scores"]), |
| ] |
| if args.attn_fp32: |
| sub += [ |
| helper.make_node("Cast", [p + "scores"], [p + "scores32"], to=TensorProto.FLOAT), |
| helper.make_node("Add", [p + "scores32", mask], [p + "scores_masked"]), |
| helper.make_node("Softmax", [p + "scores_masked"], [p + "probs32"], axis=-1), |
| helper.make_node("Cast", [p + "probs32"], [p + "probs"], to=TensorProto.FLOAT16), |
| ] |
| else: |
| sub += [ |
| helper.make_node("Add", [p + "scores", mask], [p + "scores_masked"]), |
| helper.make_node("Softmax", [p + "scores_masked"], [p + "probs"], axis=-1), |
| ] |
| sub += [ |
| helper.make_node("MatMul", [p + "probs", p + "vT"], [p + "ctx"]), |
| helper.make_node("Transpose", [p + "ctx"], [out], perm=[1, 0, 2]), |
| ] |
| for s in sub: |
| s.name = s.output[0] |
| out_nodes.extend(sub) |
| replaced += 1 |
|
|
| |
| consumed = {i for n in out_nodes for i in n.input} |
| out_nodes = [n for n in out_nodes if not (n.op_type == "Constant" and n.output[0].startswith("trt::ViTAttentionPlugin") and n.output[0] not in consumed)] |
|
|
| del g.node[:]; g.node.extend(new_nodes + out_nodes) |
| g.initializer.extend(consts) |
| keep = [o for o in m.opset_import if o.domain != "trt"] |
| del m.opset_import[:]; m.opset_import.extend(keep) |
| del g.value_info[:] |
|
|
| gs_graph = gs.import_onnx(m) |
| gs_graph.toposort().cleanup() |
| m = gs.export_onnx(gs_graph) |
| m.ir_version = 11 |
| onnx.checker.check_model(m) |
| os.makedirs(os.path.dirname(args.dst), exist_ok=True) |
| onnx.save(m, args.dst, save_as_external_data=True, all_tensors_to_one_file=True, |
| location=os.path.basename(args.dst) + ".data", size_threshold=1024) |
| print(f"Replaced {replaced} plugin nodes. Saved {args.dst}") |
| print("inputs :", [(i.name, TensorProto.DataType.Name(i.type.tensor_type.elem_type)) for i in m.graph.input]) |
| print("outputs:", [(o.name, TensorProto.DataType.Name(o.type.tensor_type.elem_type)) for o in m.graph.output]) |
| print(Counter(n.op_type for n in m.graph.node).most_common(14)) |
|
|