Files
nvidia--tensorrt/.agents/skills/trt-perf-analysis/scripts/trt_perf/layer_info.py
T
wehub-resource-sync c8a779b1bb
Docker Image CI / build-ubuntu2004 (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:55 +08:00

537 lines
17 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Process TensorRT layer-info JSON and write visualization targets."""
from __future__ import annotations
import argparse
import importlib
import json
import re
import sys
from collections import Counter
from dataclasses import dataclass
from pathlib import Path
from typing import Any
STANDARD_LAYER_KEYS = {
"Name",
"LayerType",
"Inputs",
"Constants",
"Outputs",
"TacticName",
"StreamId",
"Metadata",
"_subgraph",
}
@dataclass(frozen=True)
class ProcessedInitializer:
name: str
desc: dict[str, Any]
@dataclass(frozen=True)
class ProcessedNode:
name: str
op_type: str
inputs: list[str]
outputs: list[str]
attrs: dict[str, Any]
@dataclass(frozen=True)
class ProcessedLayerInfo:
source_path: Path
layers: list[dict[str, Any]]
nodes: list[ProcessedNode]
graph_inputs: list[str]
graph_outputs: list[str]
graph_value_info: list[str]
value_descriptors: dict[str, dict[str, Any]]
initializers: list[ProcessedInitializer]
layer_type_counts: Counter[str]
node_op_type_counts: Counter[str]
extra_layer_fields: list[str]
@dataclass(frozen=True)
class TargetSpec:
name: str
module_name: str
default_suffix: str
description: str
aliases: tuple[str, ...] = ()
TARGET_SPECS = (
TargetSpec(
name="html",
module_name="trt_perf.layer_info_html",
default_suffix=".html",
description="Standalone HTML layer graph summary",
),
)
TARGETS_BY_NAME: dict[str, TargetSpec] = {}
for target_spec in TARGET_SPECS:
TARGETS_BY_NAME[target_spec.name] = target_spec
for alias in target_spec.aliases:
TARGETS_BY_NAME[alias] = target_spec
class NameRegistry:
"""Keep target value names valid under SSA while preserving TRT names."""
def __init__(self) -> None:
self.used: set[str] = set()
def reserve(self, preferred: Any, fallback: str) -> str:
base = str(preferred).strip() if preferred is not None else ""
if not base:
base = fallback
candidate = base
suffix = 1
while candidate in self.used:
candidate = f"{base}__trt_dup{suffix}"
suffix += 1
self.used.add(candidate)
return candidate
def compact_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def load_layers(path: Path) -> list[dict[str, Any]]:
with path.open("r", encoding="utf-8") as handle:
payload = json.load(handle)
if isinstance(payload, list):
layers = payload
elif isinstance(payload, dict):
for key in ("Layers", "layers", "LayerInfo", "layerInfo"):
if isinstance(payload.get(key), list):
layers = payload[key]
break
else:
raise ValueError("JSON object does not contain a layer list")
else:
raise ValueError("top-level JSON value must be a list or object")
bad_indexes = [idx for idx, layer in enumerate(layers) if not isinstance(layer, dict)]
if bad_indexes:
preview = ", ".join(str(idx) for idx in bad_indexes[:5])
raise ValueError(f"layer entries must be objects; invalid indexes: {preview}")
return layers
def as_descriptor_list(value: Any) -> list[dict[str, Any]]:
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, dict)]
def tensor_name(desc: dict[str, Any], fallback: str) -> str:
name = desc.get("Name")
if name is None or str(name).strip() == "":
return fallback
return str(name)
def safe_dim_param(text: Any, fallback: str) -> str:
value = re.sub(r"[^0-9A-Za-z_]+", "_", str(text)).strip("_")
if not value:
value = fallback
if value[0].isdigit():
value = f"d_{value}"
return value
def tensor_shape(desc: dict[str, Any], tensor: str) -> list[int | str] | None:
dims = desc.get("Dimensions")
if dims is None:
return None
if not isinstance(dims, list):
return None
shape: list[int | str] = []
prefix = safe_dim_param(tensor, "tensor")
for idx, dim in enumerate(dims):
if isinstance(dim, bool):
shape.append(int(dim))
elif isinstance(dim, int) and dim >= 0:
shape.append(dim)
else:
shape.append(f"{prefix}_dim{idx}")
return shape
def sanitize_op_type(layer_type: Any) -> str:
text = str(layer_type).strip() if layer_type is not None else ""
op_type = re.sub(r"[^0-9A-Za-z_]+", "_", text).strip("_")
if not op_type:
op_type = "Layer"
if not re.match(r"[A-Za-z_]", op_type[0]):
op_type = f"_{op_type}"
return op_type[:200]
KGEN_TOKEN_MAP = {
"and": "And",
"add": "Add",
"cast": "Cast",
"div": "Div",
"erf": "Erf",
"gath": "Gather",
"gemm": "Gemm",
"mean": "Mean",
"mha": "MHA",
"move": "Move",
"mul": "Mul",
"resh": "Reshape",
"sele": "Select",
"slic": "Slice",
"sqrt": "Sqrt",
"sub": "Sub",
"tanh": "Tanh",
"tran": "Transpose",
}
def strip_kgen_signature_noise(text: str) -> str:
text = re.sub(r"_0x[0-9A-Fa-f]+$", "", text)
text = re.sub(r"_myl\d+(?:_\d+)?$", "", text)
text = re.sub(r"^__?myl_", "", text)
return text.strip("_")
def split_kgen_signature(signature: str) -> list[str]:
if not signature:
return []
if "_" in signature:
return [part for part in signature.split("_") if part]
return re.findall(r"[A-Z][a-z0-9]*|[a-z0-9]+", signature)
def normalize_kgen_token(token: str) -> str:
lowered = token.lower()
if lowered in KGEN_TOKEN_MAP:
return KGEN_TOKEN_MAP[lowered]
if lowered.startswith("v") and lowered[1:].isdigit():
return lowered.upper()
return token[:1].upper() + token[1:]
def kgen_details(layer: dict[str, Any]) -> tuple[str, list[str], str]:
candidates = [str(layer.get("TacticName", "")), layer_name(layer, 0)]
for candidate in candidates:
signature = strip_kgen_signature_noise(candidate)
tokens = [normalize_kgen_token(token) for token in split_kgen_signature(signature)]
if tokens:
return signature, tokens, f"KGEN_{'_'.join(tokens)}"
return "", [], "kgen"
def node_op_type(layer: dict[str, Any]) -> tuple[str, dict[str, Any]]:
raw_layer_type = layer.get("LayerType", "Layer")
if str(raw_layer_type).lower() != "kgen":
return sanitize_op_type(raw_layer_type), {}
signature, tokens, op_type = kgen_details(layer)
attrs: dict[str, Any] = {}
if signature:
attrs["trt_kgen_signature"] = signature
if tokens:
attrs["trt_kgen_ops_json"] = compact_json(tokens)
attrs["trt_kgen_op_count"] = len(tokens)
return sanitize_op_type(op_type), attrs
def layer_name(layer: dict[str, Any], index: int) -> str:
raw = layer.get("Name")
if raw is None or str(raw).strip() == "":
return f"trt_layer_{index}"
return str(raw)
def int_attr(value: Any, default: int = 0) -> int:
if isinstance(value, bool):
return int(value)
if isinstance(value, int):
return value
try:
return int(str(value))
except (TypeError, ValueError):
return default
def process_layers(layers: list[dict[str, Any]], source_path: Path) -> ProcessedLayerInfo:
registry = NameRegistry()
external_by_original: dict[str, str] = {}
latest_by_original: dict[str, str] = {}
descriptors_by_value: dict[str, dict[str, Any]] = {}
initializers_by_original: dict[str, str] = {}
initializers: list[ProcessedInitializer] = []
produced_values: list[str] = []
consumed_values: set[str] = set()
nodes: list[ProcessedNode] = []
def map_external(desc: dict[str, Any], fallback: str) -> str:
original = tensor_name(desc, fallback)
if original not in external_by_original:
value_name = registry.reserve(original, fallback)
external_by_original[original] = value_name
descriptors_by_value[value_name] = desc
return external_by_original[original]
def map_input(desc: dict[str, Any], fallback: str) -> str:
original = tensor_name(desc, fallback)
value_name = latest_by_original.get(original)
if value_name is None:
value_name = map_external(desc, fallback)
descriptors_by_value.setdefault(value_name, desc)
consumed_values.add(value_name)
return value_name
def map_constant(desc: dict[str, Any], fallback: str) -> str:
original = tensor_name(desc, fallback)
if original not in initializers_by_original:
value_name = registry.reserve(original, fallback)
initializers_by_original[original] = value_name
descriptors_by_value[value_name] = desc
initializers.append(ProcessedInitializer(value_name, desc))
value_name = initializers_by_original[original]
descriptors_by_value.setdefault(value_name, desc)
consumed_values.add(value_name)
return value_name
def map_output(desc: dict[str, Any], fallback: str) -> str:
original = tensor_name(desc, fallback)
value_name = registry.reserve(original, fallback)
latest_by_original[original] = value_name
descriptors_by_value[value_name] = desc
produced_values.append(value_name)
return value_name
layer_type_counts: Counter[str] = Counter()
node_op_type_counts: Counter[str] = Counter()
unmapped_layer_fields: set[str] = set()
for index, layer in enumerate(layers):
inputs = as_descriptor_list(layer.get("Inputs"))
constants = as_descriptor_list(layer.get("Constants"))
outputs = as_descriptor_list(layer.get("Outputs"))
node_inputs = [
map_input(desc, f"trt_layer_{index}_input_{pos}")
for pos, desc in enumerate(inputs)
]
node_inputs.extend(
map_constant(desc, f"trt_layer_{index}_constant_{pos}")
for pos, desc in enumerate(constants)
)
node_outputs = [
map_output(desc, f"trt_layer_{index}_output_{pos}")
for pos, desc in enumerate(outputs)
]
raw_layer_type = layer.get("LayerType", "Layer")
layer_type_counts[str(raw_layer_type)] += 1
op_type, op_type_attrs = node_op_type(layer)
node_op_type_counts[op_type] += 1
extra = {key: value for key, value in layer.items() if key not in STANDARD_LAYER_KEYS}
unmapped_layer_fields.update(extra)
attrs: dict[str, Any] = {
"trt_layer_index": index,
"trt_layer_type": str(raw_layer_type),
"trt_input_count": len(inputs),
"trt_constant_count": len(constants),
"trt_output_count": len(outputs),
"trt_inputs_json": compact_json(inputs),
"trt_constants_json": compact_json(constants),
"trt_outputs_json": compact_json(outputs),
}
attrs.update(op_type_attrs)
if "TacticName" in layer:
attrs["trt_tactic_name"] = str(layer.get("TacticName", ""))
if "StreamId" in layer:
attrs["trt_stream_id"] = int_attr(layer.get("StreamId"))
if "_subgraph" in layer:
attrs["trt_subgraph"] = int_attr(layer.get("_subgraph"))
if "Metadata" in layer:
attrs["trt_metadata"] = str(layer.get("Metadata", ""))
if extra:
attrs["trt_extra_json"] = compact_json(extra)
nodes.append(
ProcessedNode(
name=layer_name(layer, index),
op_type=op_type,
inputs=node_inputs,
outputs=node_outputs,
attrs=attrs,
)
)
terminal_values = [value_name for value_name in produced_values if value_name not in consumed_values]
if not terminal_values and produced_values:
terminal_values = [produced_values[-1]]
graph_output_names = set(terminal_values)
graph_input_names = set(external_by_original.values())
graph_value_info = [
value_name
for value_name in descriptors_by_value
if value_name not in graph_input_names and value_name not in graph_output_names
]
return ProcessedLayerInfo(
source_path=source_path,
layers=layers,
nodes=nodes,
graph_inputs=list(external_by_original.values()),
graph_outputs=terminal_values,
graph_value_info=graph_value_info,
value_descriptors=descriptors_by_value,
initializers=initializers,
layer_type_counts=layer_type_counts,
node_op_type_counts=node_op_type_counts,
extra_layer_fields=sorted(unmapped_layer_fields),
)
def load_and_process_layer_info(path: Path) -> ProcessedLayerInfo:
return process_layers(load_layers(path), path)
def resolve_target_spec(target: str) -> TargetSpec:
try:
return TARGETS_BY_NAME[target]
except KeyError as exc:
valid = ", ".join(sorted(TARGETS_BY_NAME))
raise ValueError(f"unknown target {target!r}; valid targets: {valid}") from exc
def default_output_path(input_path: Path, target: str = "html") -> Path:
spec = resolve_target_spec(target)
return input_path.with_name(f"{input_path.stem}{spec.default_suffix}")
def target_names_help() -> str:
return ", ".join(spec.name for spec in TARGET_SPECS)
def load_target_module(spec: TargetSpec) -> Any:
return importlib.import_module(spec.module_name)
def write_target(processed: ProcessedLayerInfo, spec: TargetSpec, output_path: Path) -> None:
module = load_target_module(spec)
writer = getattr(module, "write", None)
if writer is None:
raise NotImplementedError(
f"target {spec.name!r} is not implemented yet; "
f"expected write(processed, output_path) in {spec.module_name}"
)
writer(processed, output_path)
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Process TensorRT layer-info JSON and write one or more visualization targets."
)
parser.add_argument("input", type=Path, nargs="?", help="Path to TensorRT layer-info JSON")
parser.add_argument(
"-t",
"--target",
action="append",
dest="targets",
metavar="TARGET",
help=f"Output target; repeat for multiple outputs. Default: html. Available: {target_names_help()}",
)
parser.add_argument(
"-o",
"--output",
type=Path,
help="Output path. Only valid when writing one target.",
)
parser.add_argument(
"--output-dir",
type=Path,
help="Directory for target-specific default filenames.",
)
parser.add_argument(
"--list-targets",
action="store_true",
help="List supported target names and exit.",
)
return parser.parse_args(argv)
def list_targets() -> None:
seen: set[str] = set()
for spec in TARGET_SPECS:
aliases = f" (aliases: {', '.join(spec.aliases)})" if spec.aliases else ""
print(f"{spec.name}{aliases}: {spec.description}")
seen.add(spec.name)
alias_only = sorted(name for name, spec in TARGETS_BY_NAME.items() if spec.name not in seen)
if alias_only:
print(f"aliases: {', '.join(alias_only)}")
def resolve_requested_targets(targets: list[str] | None) -> list[TargetSpec]:
requested = targets or ["html"]
specs: list[TargetSpec] = []
seen: set[str] = set()
for target in requested:
spec = resolve_target_spec(target)
if spec.name in seen:
continue
specs.append(spec)
seen.add(spec.name)
return specs
def main(argv: list[str] | None = None) -> int:
args = parse_args(sys.argv[1:] if argv is None else argv)
if args.list_targets:
list_targets()
return 0
if args.input is None:
print("error: input is required unless --list-targets is used", file=sys.stderr)
return 1
try:
target_specs = resolve_requested_targets(args.targets)
if args.output is not None and len(target_specs) != 1:
raise ValueError("--output can only be used when writing one target")
processed = load_and_process_layer_info(args.input)
written: list[Path] = []
for spec in target_specs:
if args.output is not None:
output_path = args.output
elif args.output_dir is not None:
output_path = args.output_dir / f"{args.input.stem}{spec.default_suffix}"
else:
output_path = default_output_path(args.input, spec.name)
write_target(processed, spec, output_path)
written.append(output_path)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
for output_path in written:
print(f"wrote {output_path}")
return 0