Files
zaochuan5854 ef9b7fac4c feat(definitions): add TensorRT definitions and model metadata
- Created definitions.py to encapsulate TensorRT runtime setup, model metadata, and type definitions.
- Updated anima_diffuser.py to import WeightsNameMap and WeightsShapeMap from definitions.py.
- Refactored base_diffuser.py to import necessary types and runtime from definitions.py, removing redundant definitions.
- Modified trt_exporter.py to utilize definitions for model metadata and weight mappings, enhancing code organization.
- Adjusted trt_loader.py to import types from definitions.py, streamlining dependencies.
- Updated trt_utils.py to use shared runtime from definitions.py, ensuring consistent TensorRT usage across modules.
2026-04-23 18:22:24 +09:00

440 lines
17 KiB
Python
Executable File

import os
import onnx
import enum
import json
import shutil
import mmap
import gc
import torch
import tensorrt as trt
import onnxruntime as ort
import folder_paths
from onnx import helper, TensorProto
from onnx.onnx_pb import ModelProto
from typing import overload, Literal, Any, Optional, TypeVar
from .definitions import WeightsNameMap, WeightsShapeMap, trt_runtime
T = TypeVar("T")
class BundleEntryType(enum.Enum):
TRT_ENGINE = 1
ONNX_MODEL = 2
WEIGHTS_MAP = 3
class ModelBundle:
def __init__(self, bundle_path: str):
self.bundle_path = ensure_temp_or_output_path(bundle_path)
self._file = open(self.bundle_path, "r+b")
self._mm: Optional[mmap.mmap] = None
self._entry_views: dict[BundleEntryType, memoryview] = {}
self._reload_views()
def _open_views(self):
if self._mm is None:
raise RuntimeError("mmap is not initialized")
self._entry_views.clear()
self._mv = memoryview(self._mm)
file_size = len(self._mv)
if file_size < 8:
raise ValueError(f"Invalid bundle file (too small): {self.bundle_path}")
cursor = file_size - 8
meta_size = int.from_bytes(self._mv[cursor: cursor + 8], "little")
cursor -= meta_size
if cursor < 0:
raise ValueError(f"Invalid bundle metadata size in: {self.bundle_path}")
self._meta_offset = cursor
self._meta_view = self._mv[self._meta_offset: self._meta_offset + meta_size]
# Parse chunks backward from metadata start.
# Chunk layout is: [Data][Size:8B][Type:1B]
while cursor > 0:
if cursor < 9:
raise ValueError(f"Corrupted bundle chunk footer in: {self.bundle_path}")
cursor -= 1
entry_type = BundleEntryType(int.from_bytes(self._mv[cursor:cursor + 1], "little"))
cursor -= 8
entry_size = int.from_bytes(self._mv[cursor:cursor + 8], "little")
cursor -= entry_size
if cursor < 0:
raise ValueError(f"Corrupted bundle chunk size in: {self.bundle_path}")
if entry_type not in self._entry_views:
self._entry_views[entry_type] = self._mv[cursor: cursor + entry_size]
def _reload_views(self):
self.close_views()
self._close_mmap()
if os.path.getsize(self.bundle_path) == 0:
raise ValueError(f"Invalid bundle file (empty): {self.bundle_path}")
self._mm = mmap.mmap(self._file.fileno(), 0, access=mmap.ACCESS_READ)
self._open_views()
@overload
def __getitem__(self, entry: Literal[BundleEntryType.TRT_ENGINE]) -> trt.ICudaEngine:
...
@overload
def __getitem__(self, entry: Literal[BundleEntryType.ONNX_MODEL]) -> ModelProto:
...
@overload
def __getitem__(self, entry: Literal[BundleEntryType.WEIGHTS_MAP]) -> tuple[WeightsNameMap, WeightsShapeMap]:
...
def __getitem__(self, entry: BundleEntryType) -> Any:
if entry not in self._entry_views:
raise KeyError(f"Entry {entry} not found in bundle")
raw_view = self._entry_views[entry]
match entry:
case BundleEntryType.TRT_ENGINE:
return trt_runtime.deserialize_cuda_engine(raw_view) # type: ignore
case BundleEntryType.ONNX_MODEL:
# Need to copy to bytes to avoid memoryview being released (which would cause the engine/model to become invalid)
return onnx.load_model_from_string(raw_view.tobytes()) # pyright: ignore[reportUnknownMemberType]
case BundleEntryType.WEIGHTS_MAP:
payload = json.loads(raw_view.tobytes().decode("utf-8"))
weights_name = payload.get("weights_name", {})
raw_shape = payload.get("weights_shape", {})
weights_shape: WeightsShapeMap = {
k: (tuple(v[0]), bool(v[1])) for k, v in raw_shape.items()
}
return weights_name, weights_shape
case _:
raise KeyError(f"Unknown entry type: {entry}")
@overload
def get(self, entry: Literal[BundleEntryType.TRT_ENGINE], default: T = None) -> trt.ICudaEngine | T:
...
@overload
def get(self, entry: Literal[BundleEntryType.ONNX_MODEL], default: T = None) -> ModelProto | T:
...
@overload
def get(self, entry: Literal[BundleEntryType.WEIGHTS_MAP], default: T = None) -> tuple[WeightsNameMap, WeightsShapeMap] | T:
...
def get(self, entry: BundleEntryType, default: Any = None) -> Any:
if entry in self._entry_views:
return self[entry]
return default
def save_weights_mapping(self, weights_name: WeightsNameMap, weights_shape: WeightsShapeMap):
payload: dict[str, Any] = {
"weights_name": weights_name,
"weights_shape": {
k: [list(shape), is_transpose]
for k, (shape, is_transpose) in weights_shape.items()
},
}
self.append_entry(BundleEntryType.WEIGHTS_MAP, json.dumps(payload).encode("utf-8"))
def load_weights_mapping(self) -> tuple[WeightsNameMap, WeightsShapeMap]:
if BundleEntryType.WEIGHTS_MAP not in self._entry_views:
raise KeyError("weights mapping is not stored in bundle")
return self[BundleEntryType.WEIGHTS_MAP]
def append_entry(self, entry_type: BundleEntryType, data: bytes):
metadata_bytes = self._meta_view.tobytes()
self.close_views()
self._close_mmap()
self._file.seek(self._meta_offset)
self._file.write(data)
self._file.write(len(data).to_bytes(8, "little"))
self._file.write(entry_type.value.to_bytes(1, "little"))
self._file.write(metadata_bytes)
self._file.write(len(metadata_bytes).to_bytes(8, "little"))
self._file.truncate()
self._file.flush()
self._reload_views()
def __contains__(self, key: BundleEntryType) -> bool:
return key in self._entry_views
def __keys__(self):
return self._entry_views.keys()
@property
def metadata(self) -> dict[Any, Any]:
if not hasattr(self, "_cached_metadata"):
# Parse metadata JSON if not cached. If meta_view is empty, treat as empty dict.
if len(self._meta_view) == 0:
self._cached_metadata = {}
else:
self._cached_metadata = json.loads(self._meta_view.tobytes().decode("utf-8"))
return self._cached_metadata
@metadata.setter
def metadata(self, value: dict[Any, Any]):
raw = json.dumps(value).encode("utf-8")
new_metadata_size = len(raw)
new_size = self._meta_offset + new_metadata_size + 8
self.close_views()
self._close_mmap()
self._file.truncate(new_size)
self._file.seek(self._meta_offset)
self._file.write(raw)
self._file.write(new_metadata_size.to_bytes(8, "little"))
self._file.flush()
self._reload_views()
self._cached_metadata = value
@staticmethod
def _serialize_onnx_model(onnx_path: str):
onnx_path = ensure_temp_or_output_path(onnx_path)
os.makedirs(os.path.dirname(onnx_path), exist_ok=True)
onnx_data_file = os.path.join(os.path.dirname(onnx_path), os.path.splitext(os.path.basename(onnx_path))[0] + ".onnx.data")
if os.path.exists(onnx_data_file):
if os.path.getsize(onnx_path) + os.path.getsize(onnx_data_file) < 1.5 * 1024 * 1024 * 1024:
# To sirialize ONNX model with external data, we need to load and re-serialize it, which can be memory intensive.
# If the model data file is larger than 1.5GB, we will raise an error for now to avoid potential OOM. Future optimization can be done by directly copying the external data file and adjusting the ONNX model's external data reference without fully loading it into memory.
model = onnx.load(onnx_path) # pyright: ignore[reportUnknownMemberType]
onnx_bytes = model.SerializeToString()
with open(onnx_path, "wb") as f_out:
f_out.write(onnx_bytes)
f_out.flush()
return onnx_path
else:
raise NotImplementedError(f"ONNX model with large external data file (>1.5GB) is not supported yet: {onnx_data_file}")
@classmethod
def from_onnx(cls, onnx_path: str, output_path: str, replace_source: bool = True):
onnx_path = ensure_temp_or_output_path(onnx_path)
output_path = ensure_temp_or_output_path(output_path)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
cls._serialize_onnx_model(onnx_path)
if replace_source:
os.rename(onnx_path, output_path)
byte_length = os.path.getsize(output_path).to_bytes(8, "little")
with open(output_path, "ab") as f_out:
f_out.write(byte_length)
f_out.write(BundleEntryType.ONNX_MODEL.value.to_bytes(1, "little"))
f_out.write((0).to_bytes(8, "little")) # meta size 0
f_out.flush()
else:
byte_length = os.path.getsize(onnx_path).to_bytes(8, "little")
with open(onnx_path, "rb") as f_source, open(output_path, "wb") as f_out:
shutil.copyfileobj(f_source, f_out)
f_out.write(byte_length)
f_out.write(BundleEntryType.ONNX_MODEL.value.to_bytes(1, "little"))
f_out.write((0).to_bytes(8, "little")) # meta size 0
f_out.flush()
return cls(output_path)
@classmethod
def from_trt_engine(cls, trt_engine_path: str, output_path: str, replace_source: bool = True):
trt_engine_path = ensure_temp_or_output_path(trt_engine_path)
output_path = ensure_temp_or_output_path(output_path)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
trt_byte_length = os.path.getsize(trt_engine_path).to_bytes(8, "little")
if replace_source:
with open(trt_engine_path, "ab") as f:
f.write(trt_byte_length)
f.write(BundleEntryType.TRT_ENGINE.value.to_bytes(1, "little"))
f.write((0).to_bytes(8, "little")) # meta size 0
f.flush()
os.rename(trt_engine_path, output_path)
else:
with open(trt_engine_path, "rb") as f_source, open(output_path, "wb") as f_out:
shutil.copyfileobj(f_source, f_out)
f_out.write(trt_byte_length)
f_out.write(BundleEntryType.TRT_ENGINE.value.to_bytes(1, "little"))
f_out.write((0).to_bytes(8, "little")) # meta size 0
f_out.flush()
return cls(output_path)
@classmethod
def from_onnx_and_trt_engine(cls, onnx_path: str, trt_engine_path: str, output_path: str, replace_source: bool = True) -> "ModelBundle":
onnx_path = ensure_temp_or_output_path(onnx_path)
trt_engine_path = ensure_temp_or_output_path(trt_engine_path)
output_path = ensure_temp_or_output_path(output_path)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
cls._serialize_onnx_model(onnx_path)
len_onnx_bytes = os.path.getsize(onnx_path)
len_trt_bytes = os.path.getsize(trt_engine_path)
if len_onnx_bytes >= len_trt_bytes:
base_path, source_path = onnx_path, trt_engine_path
base_type, source_type = BundleEntryType.ONNX_MODEL, BundleEntryType.TRT_ENGINE
base_size, source_size = len_onnx_bytes, len_trt_bytes
else:
base_path, source_path = trt_engine_path, onnx_path
base_type, source_type = BundleEntryType.TRT_ENGINE, BundleEntryType.ONNX_MODEL
base_size, source_size = len_trt_bytes, len_onnx_bytes
os.rename(base_path, output_path)
with open(output_path, "ab") as f_base, open(source_path, "rb") as f_source:
f_base.write(base_size.to_bytes(8, "little"))
f_base.write(base_type.value.to_bytes(1, "little"))
shutil.copyfileobj(f_source, f_base)
f_base.write(source_size.to_bytes(8, "little"))
f_base.write(source_type.value.to_bytes(1, "little"))
# Writing empty metadata
f_base.write((0).to_bytes(8, "little")) # meta size 0
f_base.flush()
return cls(output_path)
def close_views(self):
for view in self._entry_views.values():
try:
view.release()
except ValueError:
pass
self._entry_views.clear()
if hasattr(self, "_meta_view"):
try:
self._meta_view.release()
except ValueError:
pass
del self._meta_view
if hasattr(self, "_mv"):
try:
self._mv.release()
except ValueError:
pass
del self._mv
def _close_mmap(self):
if self._mm is None:
return
try:
self._mm.close()
except BufferError:
# Be defensive for lingering exported pointers.
gc.collect()
self._mm.close()
finally:
self._mm = None
def close(self):
self.close_views()
self._close_mmap()
if hasattr(self, "_file"):
self._file.close()
del self._file
def __del__(self):
self.close()
# ## File Layout
# Each data chunk's role is defined by its leading ID. The appearance order within the file is arbitrary.
# +-----------------------------------------+ <--- Offset 0
# | [ID:1B][Size:8B][Chunk Data...] | Data Chunk A
# +-----------------------------------------+
# | [ID:1B][Size:8B][Chunk Data...] | Data Chunk B
# +-----------------------------------------+
# | ... | (Additional Chunks in any order)
# +-----------------------------------------+ <--- End of Data Chunks (data_limit)
# | |
# | Metadata Section (JSON) | Variable Length (No ID prefix)
# | |
# +-----------------------------------------+ <--- Metadata End (EOF - 8 bytes)
# | Metadata Size (8 bytes) | uint64, Little Endian
# +-----------------------------------------+ <--- EOF
# ---
# ## ID Definition
# - 0x01: TensorRT Engine Data
# - 0x02: ONNX Model Data
# - 0x03: WeightsMap (JSON / Binary)
# - 0x04-0xFF: Reserved for future extensions
# ---
# ## Parsing Logic (ID-Driven)
# 1. Read the last 8 bytes of the file to get `meta_size`.
# 2. Calculate `data_limit` = (EOF - 8 - meta_size).
# 3. Initialize `current_offset = 0`.
# 4. While `current_offset < data_limit`:
# a. Read 1 byte as `chunk_id`.
# b. Read 8 bytes as `chunk_size`.
# c. Record `data_start = current_offset + 9`.
# d. Store the mapping of `chunk_id` -> `data_start`.
# e. Jump to the next chunk: `current_offset += (9 + chunk_size)`.
# 5. Seek to `data_limit` and parse the Metadata JSON.
def ensure_temp_or_output_path(path_value: str) -> str:
def _is_within_root(path_value: str, root: str) -> bool:
safe_path = os.path.normcase(os.path.realpath(path_value))
safe_root = os.path.normcase(os.path.realpath(root))
try:
return os.path.commonpath([safe_root, safe_path]) == safe_root
except ValueError:
# Different drives on Windows or invalid path combinations.
return False
if _is_within_root(path_value, folder_paths.get_output_directory()):
return os.path.realpath(path_value)
if _is_within_root(path_value, folder_paths.get_temp_directory()):
return os.path.realpath(path_value)
raise ValueError(f"Path is outside temp/output roots: {path_value}")
def trt_datatype_to_torch(datatype: trt.DataType) -> torch.dtype:
type_map = {
trt.DataType.FLOAT: torch.float32,
trt.DataType.HALF: torch.float16,
trt.DataType.INT32: torch.int32,
trt.DataType.BOOL: torch.bool,
}
if hasattr(trt.DataType, "BF16"):
type_map[trt.DataType.BF16] = torch.bfloat16
if datatype in type_map:
return type_map[datatype]
raise ValueError(f"Unsupported TensorRT datatype: {datatype}")
def torch_dtype_to_trt(datatype: torch.dtype) -> trt.DataType:
type_map = {
torch.float32: trt.DataType.FLOAT,
torch.float16: trt.DataType.HALF,
torch.int32: trt.DataType.INT32,
torch.bool: trt.DataType.BOOL,
}
if hasattr(torch, "bfloat16"):
type_map[torch.bfloat16] = trt.DataType.BF16
if datatype in type_map:
return type_map[datatype]
raise ValueError(f"Unsupported torch dtype for TensorRT conversion: {datatype}")
def check_cuda_compatibility() -> bool:
"""
Check if CUDA is available and compatible with ONNX Runtime."""
node = helper.make_node("Add", ["A", "B"], ["C"])
graph = helper.make_graph([node], "test", [
helper.make_tensor_value_info("A", TensorProto.DataType.FLOAT, [1]),
helper.make_tensor_value_info("B", TensorProto.DataType.FLOAT, [1])
], [helper.make_tensor_value_info("C", TensorProto.DataType.FLOAT, [1])])
model = helper.make_model(graph)
try:
sess = ort.InferenceSession(model.SerializeToString(), providers=["CUDAExecutionProvider"]) # type: ignore
return True
except Exception as e:
print(f"[Warning] CUDA found but incompatible: {e}")
return False