|
|
|
|
| import json
|
| from pathlib import Path
|
|
|
| import torch
|
|
|
| from ultralytics.utils import IS_JETSON, LOGGER
|
|
|
|
|
| def export_onnx(
|
| torch_model,
|
| im,
|
| onnx_file,
|
| opset=14,
|
| input_names=["images"],
|
| output_names=["output0"],
|
| dynamic=False,
|
| ):
|
| """
|
| Exports a PyTorch model to ONNX format.
|
|
|
| Args:
|
| torch_model (torch.nn.Module): The PyTorch model to export.
|
| im (torch.Tensor): Example input tensor for the model.
|
| onnx_file (str): Path to save the exported ONNX file.
|
| opset (int): ONNX opset version to use for export.
|
| input_names (list): List of input tensor names.
|
| output_names (list): List of output tensor names.
|
| dynamic (bool | dict, optional): Whether to enable dynamic axes. Defaults to False.
|
|
|
| Notes:
|
| - Setting `do_constant_folding=True` may cause issues with DNN inference for torch>=1.12.
|
| """
|
| torch.onnx.export(
|
| torch_model,
|
| im,
|
| onnx_file,
|
| verbose=False,
|
| opset_version=opset,
|
| do_constant_folding=True,
|
| input_names=input_names,
|
| output_names=output_names,
|
| dynamic_axes=dynamic or None,
|
| )
|
|
|
|
|
| def export_engine(
|
| onnx_file,
|
| engine_file=None,
|
| workspace=None,
|
| half=False,
|
| int8=False,
|
| dynamic=False,
|
| shape=(1, 3, 640, 640),
|
| dla=None,
|
| dataset=None,
|
| metadata=None,
|
| verbose=False,
|
| prefix="",
|
| ):
|
| """
|
| Exports a YOLO model to TensorRT engine format.
|
|
|
| Args:
|
| onnx_file (str): Path to the ONNX file to be converted.
|
| engine_file (str, optional): Path to save the generated TensorRT engine file.
|
| workspace (int, optional): Workspace size in GB for TensorRT. Defaults to None.
|
| half (bool, optional): Enable FP16 precision. Defaults to False.
|
| int8 (bool, optional): Enable INT8 precision. Defaults to False.
|
| dynamic (bool, optional): Enable dynamic input shapes. Defaults to False.
|
| shape (tuple, optional): Input shape (batch, channels, height, width). Defaults to (1, 3, 640, 640).
|
| dla (int, optional): DLA core to use (Jetson devices only). Defaults to None.
|
| dataset (ultralytics.data.build.InfiniteDataLoader, optional): Dataset for INT8 calibration. Defaults to None.
|
| metadata (dict, optional): Metadata to include in the engine file. Defaults to None.
|
| verbose (bool, optional): Enable verbose logging. Defaults to False.
|
| prefix (str, optional): Prefix for log messages. Defaults to "".
|
|
|
| Raises:
|
| ValueError: If DLA is enabled on non-Jetson devices or required precision is not set.
|
| RuntimeError: If the ONNX file cannot be parsed.
|
|
|
| Notes:
|
| - TensorRT version compatibility is handled for workspace size and engine building.
|
| - INT8 calibration requires a dataset and generates a calibration cache.
|
| - Metadata is serialized and written to the engine file if provided.
|
| """
|
| import tensorrt as trt
|
|
|
| engine_file = engine_file or Path(onnx_file).with_suffix(".engine")
|
|
|
| logger = trt.Logger(trt.Logger.INFO)
|
| if verbose:
|
| logger.min_severity = trt.Logger.Severity.VERBOSE
|
|
|
|
|
| builder = trt.Builder(logger)
|
| config = builder.create_builder_config()
|
| workspace = int((workspace or 0) * (1 << 30))
|
| is_trt10 = int(trt.__version__.split(".")[0]) >= 10
|
| if is_trt10 and workspace > 0:
|
| config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, workspace)
|
| elif workspace > 0:
|
| config.max_workspace_size = workspace
|
| flag = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
|
| network = builder.create_network(flag)
|
| half = builder.platform_has_fast_fp16 and half
|
| int8 = builder.platform_has_fast_int8 and int8
|
|
|
|
|
| if dla is not None:
|
| if not IS_JETSON:
|
| raise ValueError("DLA is only available on NVIDIA Jetson devices")
|
| LOGGER.info(f"{prefix} enabling DLA on core {dla}...")
|
| if not half and not int8:
|
| raise ValueError(
|
| "DLA requires either 'half=True' (FP16) or 'int8=True' (INT8) to be enabled. Please enable one of them and try again."
|
| )
|
| config.default_device_type = trt.DeviceType.DLA
|
| config.DLA_core = int(dla)
|
| config.set_flag(trt.BuilderFlag.GPU_FALLBACK)
|
|
|
|
|
| parser = trt.OnnxParser(network, logger)
|
| if not parser.parse_from_file(onnx_file):
|
| raise RuntimeError(f"failed to load ONNX file: {onnx_file}")
|
|
|
|
|
| inputs = [network.get_input(i) for i in range(network.num_inputs)]
|
| outputs = [network.get_output(i) for i in range(network.num_outputs)]
|
| for inp in inputs:
|
| LOGGER.info(f'{prefix} input "{inp.name}" with shape{inp.shape} {inp.dtype}')
|
| for out in outputs:
|
| LOGGER.info(f'{prefix} output "{out.name}" with shape{out.shape} {out.dtype}')
|
|
|
| if dynamic:
|
| if shape[0] <= 1:
|
| LOGGER.warning(f"{prefix} WARNING ⚠️ 'dynamic=True' model requires max batch size, i.e. 'batch=16'")
|
| profile = builder.create_optimization_profile()
|
| min_shape = (1, shape[1], 32, 32)
|
| max_shape = (*shape[:2], *(int(max(1, workspace or 1) * d) for d in shape[2:]))
|
| for inp in inputs:
|
| profile.set_shape(inp.name, min=min_shape, opt=shape, max=max_shape)
|
| config.add_optimization_profile(profile)
|
|
|
| LOGGER.info(f"{prefix} building {'INT8' if int8 else 'FP' + ('16' if half else '32')} engine as {engine_file}")
|
| if int8:
|
| config.set_flag(trt.BuilderFlag.INT8)
|
| config.set_calibration_profile(profile)
|
| config.profiling_verbosity = trt.ProfilingVerbosity.DETAILED
|
|
|
| class EngineCalibrator(trt.IInt8Calibrator):
|
| """
|
| Custom INT8 calibrator for TensorRT.
|
|
|
| Args:
|
| dataset (object): Dataset for calibration.
|
| batch (int): Batch size for calibration.
|
| cache (str, optional): Path to save the calibration cache. Defaults to "".
|
| """
|
|
|
| def __init__(
|
| self,
|
| dataset,
|
| cache: str = "",
|
| ) -> None:
|
| trt.IInt8Calibrator.__init__(self)
|
| self.dataset = dataset
|
| self.data_iter = iter(dataset)
|
| self.algo = trt.CalibrationAlgoType.ENTROPY_CALIBRATION_2
|
| self.batch = dataset.batch_size
|
| self.cache = Path(cache)
|
|
|
| def get_algorithm(self) -> trt.CalibrationAlgoType:
|
| """Get the calibration algorithm to use."""
|
| return self.algo
|
|
|
| def get_batch_size(self) -> int:
|
| """Get the batch size to use for calibration."""
|
| return self.batch or 1
|
|
|
| def get_batch(self, names) -> list:
|
| """Get the next batch to use for calibration, as a list of device memory pointers."""
|
| try:
|
| im0s = next(self.data_iter)["img"] / 255.0
|
| im0s = im0s.to("cuda") if im0s.device.type == "cpu" else im0s
|
| return [int(im0s.data_ptr())]
|
| except StopIteration:
|
|
|
| return None
|
|
|
| def read_calibration_cache(self) -> bytes:
|
| """Use existing cache instead of calibrating again, otherwise, implicitly return None."""
|
| if self.cache.exists() and self.cache.suffix == ".cache":
|
| return self.cache.read_bytes()
|
|
|
| def write_calibration_cache(self, cache) -> None:
|
| """Write calibration cache to disk."""
|
| _ = self.cache.write_bytes(cache)
|
|
|
|
|
| config.int8_calibrator = EngineCalibrator(
|
| dataset=dataset,
|
| cache=str(Path(onnx_file).with_suffix(".cache")),
|
| )
|
|
|
| elif half:
|
| config.set_flag(trt.BuilderFlag.FP16)
|
|
|
|
|
| build = builder.build_serialized_network if is_trt10 else builder.build_engine
|
| with build(network, config) as engine, open(engine_file, "wb") as t:
|
|
|
| if metadata is not None:
|
| meta = json.dumps(metadata)
|
| t.write(len(meta).to_bytes(4, byteorder="little", signed=True))
|
| t.write(meta.encode())
|
|
|
| t.write(engine if is_trt10 else engine.serialize())
|
|
|