Files
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

492 lines
19 KiB
Python

#
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import numpy as np
import tensorrt as trt
import torch
import sys
import os
import time
import argparse
from cuda.bindings import runtime as cudart
from ctypes import py_object, pythonapi, c_void_p, c_char_p
from typing import Optional
try:
from mpi4py import MPI
except ImportError:
MPI = None
try:
import nccl.core as nccl
except ImportError:
nccl = None
sys.path.insert(1, os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
import common
def communicator_to_capsule(comm):
"""
Convert nccl.core.Communicator to PyCapsule for TensorRT compatibility.
Args:
comm: nccl.core.Communicator instance with .ptr attribute set to ncclComm_t handle
Returns:
PyCapsule wrapping the communicator pointer, suitable for set_communicator()
Raises:
ValueError: If comm.ptr is invalid (0 or None), indicating destroyed communicator
TypeError: If comm doesn't have a .ptr attribute
"""
# Validate input
if comm is None:
raise TypeError("Communicator cannot be None")
if not hasattr(comm, 'ptr'):
raise TypeError(f"Object {type(comm)} does not have 'ptr' attribute. "
"Expected nccl.core.Communicator instance.")
# Get the raw pointer from the Communicator object
ptr = comm.ptr
# Validate that communicator is still alive (ptr != 0)
if ptr == 0:
raise ValueError("NCCL Communicator has been destroyed (ptr=0). "
"Cannot create capsule for destroyed communicator.")
# Convert to PyCapsule using ctypes.pythonapi
PyCapsule_New = pythonapi.PyCapsule_New
PyCapsule_New.restype = py_object
PyCapsule_New.argtypes = [c_void_p, c_char_p, c_void_p]
capsule = PyCapsule_New(c_void_p(ptr), b"ncclComm_t", None)
if capsule is None:
raise RuntimeError("Failed to create PyCapsule from communicator pointer")
return capsule
def allocate_buffers(engine: trt.ICudaEngine, profile_idx: Optional[int] = None, output_shape: Optional[tuple] = None):
"""Allocate host and device buffers for TensorRT engine."""
inputs = []
outputs = []
bindings = []
tensor_names = [engine.get_tensor_name(i) for i in range(engine.num_io_tensors)]
for binding in tensor_names:
# Pick out the max shape to allocate enough memory for the binding.
shape = engine.get_tensor_shape(binding) if profile_idx is None else engine.get_tensor_profile_shape(binding, profile_idx)[-1]
shape_valid = np.all([s >= 0 for s in shape])
if not shape_valid and profile_idx is None:
raise ValueError(f"Binding {binding} has dynamic shape, " +\
"but no profile was specified.")
# For dynamic shapes, use fixed output shape
if output_shape is not None:
shape = output_shape
size = trt.volume(shape)
trt_type = engine.get_tensor_dtype(binding)
# Allocate host and device buffers
if trt_type == trt.DataType.BF16:
dtype = np.dtype(np.uint16)
bindingMemory = common.HostDeviceMem(size, dtype)
elif trt_type == trt.DataType.HALF:
dtype = np.dtype(np.uint16)
bindingMemory = common.HostDeviceMem(size, dtype)
elif trt_type == trt.DataType.FLOAT:
dtype = np.dtype(np.float32)
bindingMemory = common.HostDeviceMem(size, dtype)
else:
try:
dtype = np.dtype(trt.nptype(trt_type))
bindingMemory = common.HostDeviceMem(size, dtype)
except TypeError:
size = int(size * trt_type.itemsize)
bindingMemory = common.HostDeviceMem(size)
# Append the device buffer to device bindings.
bindings.append(int(bindingMemory.device_ptr))
# Append to the appropriate list.
if engine.get_tensor_mode(binding) == trt.TensorIOMode.INPUT:
inputs.append(bindingMemory)
else:
outputs.append(bindingMemory)
return inputs, outputs, bindings
class AttentionSD:
"""Base class for Attention model using TensorRT (Single Device)"""
def __init__(self, mpi_comm, rank, onnx_path):
"""
Initialize the Attention class
Args:
mpi_comm: MPI communicator
rank: Current instance ID
onnx_path: Path to the ONNX model
"""
self.onnx_path = onnx_path
self.logger = trt.Logger(trt.Logger.WARNING)
self.engine = None
self.context = None
self.inputs = None
self.outputs = None
self.bindings = None
self.mpi_comm = mpi_comm
self.rank = rank
def setup(self, actual_input_shape, output_shape):
"""
Set up everything before doing inference.
"""
engine_string = self.build_serialized_network()
self.engine = trt.Runtime(self.logger).deserialize_cuda_engine(engine_string)
if self.engine is None:
print("Failed deserializing engine!")
exit(-1)
print("Succeeded deserializing engine!")
self.context = self.engine.create_execution_context()
# For dynamic shapes, we need to specify the actual input shape we want to use
input_name = self.engine.get_tensor_name(0)
self.context.set_input_shape(input_name, actual_input_shape)
# Allocate buffers
self.inputs, self.outputs, self.bindings = allocate_buffers(
self.engine, profile_idx=0, output_shape=output_shape
)
num_io = self.engine.num_io_tensors
tensor_names = [self.engine.get_tensor_name(i) for i in range(num_io)]
for i in range(num_io):
self.context.set_tensor_address(tensor_names[i], self.bindings[i])
def build_serialized_network(self):
"""Create and serialize a network from the ONNX model."""
# Create builder and empty network
builder = trt.Builder(self.logger)
network = builder.create_network(flags=1 << int(trt.NetworkDefinitionCreationFlag.STRONGLY_TYPED))
# Setup parser and parse the ONNX model
print(f"Parsing ONNX model from {self.onnx_path}")
parser = trt.OnnxParser(network, self.logger)
with open(self.onnx_path, "rb") as f:
if not parser.parse(f.read()):
print("Failed to parse ONNX model")
for error in range(parser.num_errors):
print(parser.get_error(error))
return None
# Get input dimensions and data type
input_tensor = network.get_input(0)
input_shape = input_tensor.shape
input_name = input_tensor.name
input_dtype = input_tensor.dtype
print(f"[Rank {self.rank}] Input shape: {input_shape}")
print(f"[Rank {self.rank}] Input name: {input_name}")
print(f"[Rank {self.rank}] Input data type: {input_dtype}")
# Create a builder config
config = builder.create_builder_config()
config.profiling_verbosity = trt.ProfilingVerbosity.DETAILED
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 16 * 1024 * 1024 * 1024) # 16GB workspace
config.set_memory_pool_limit(trt.MemoryPoolType.TACTIC_SHARED_MEMORY, 1 * 1024 * 1024 * 1024) # 1GB shared mem
profile = builder.create_optimization_profile()
# Set the shape range for the input tensor
min_shape = (1, 1, 4096)
opt_shape = (56320, 1, 4096)
max_shape = (56320, 1, 4096)
profile.set_shape(input_name, min_shape, opt_shape, max_shape)
config.add_optimization_profile(profile)
# Build the serialized network
serialized_engine = builder.build_serialized_network(network, config)
if serialized_engine is None:
print(f"[Rank {self.rank}] Failed building serialized engine!")
exit(-1)
print(f"[Rank {self.rank}] Succeeded building serialized engine!")
return serialized_engine
def infer(self, input_data, output_shape, num_iterations):
"""
Execute inference on the input data.
Args:
input_data: Input data for inference
output_shape: Expected output shape for reshaping
num_iterations: Number of inference iterations for averaging timing results
Returns:
output_data: List of output tensors
"""
print(f"[Rank {self.rank}] Input shape: {input_data.shape}")
# Copy input data to device
for input_buffer in self.inputs:
common.memcpy_host_to_device(input_buffer.device_ptr, input_data)
# Warmup
with common.CudaStreamContext() as stream:
self.context.execute_async_v3(stream.stream)
common.cuda_call(cudart.cudaStreamSynchronize(stream.stream))
# Run inference
start = time.time()
for _ in range(num_iterations):
self.context.execute_async_v3(stream.stream)
common.cuda_call(cudart.cudaStreamSynchronize(stream.stream))
end = time.time()
print(f"[Rank {self.rank}] Time spent in TRT attention: {(end-start)/num_iterations * 1000} ms")
# Get output
output_data = []
for output in self.outputs:
common.memcpy_device_to_host(output.host, output.device_ptr)
# Process based on data type
if self.engine.get_tensor_dtype(self.engine.get_tensor_name(1)) == trt.DataType.BF16:
numpy_output = np.frombuffer(output.host, dtype=np.uint16).reshape(output_shape)
torch_output = torch.from_numpy(numpy_output).view(torch.bfloat16)
torch_output = torch_output.reshape(output_shape)
elif self.engine.get_tensor_dtype(self.engine.get_tensor_name(1)) == trt.DataType.HALF:
numpy_output = np.frombuffer(output.host, dtype=np.float16).reshape(output_shape)
torch_output = torch.from_numpy(numpy_output)
else:
numpy_output = np.frombuffer(output.host, dtype=np.float32).reshape(output_shape)
torch_output = torch.from_numpy(numpy_output)
output_data.append(torch_output)
return output_data
def cleanup(self):
"""
Free the buffer resources.
"""
common.free_buffers(self.inputs, self.outputs)
class AttentionMD(AttentionSD):
"""Multi-device Attention model using TensorRT with NCCL for communication"""
def __init__(self, mpi_comm, num_ranks, rank, onnx_path):
"""
Initialize the multi-device Attention class
Args:
mpi_comm: MPI communicator
num_ranks: Number of instances/devices
rank: Current instance ID
onnx_path: Path to the ONNX model
"""
super(AttentionMD, self).__init__(mpi_comm, rank, onnx_path)
self.num_ranks = num_ranks
self.nccl_comm = None
def setup_multidevice(self, root):
"""
Set up CUDA devices and initialize NCCL communicator.
Args:
root: Root rank for communication
"""
assert nccl is not None
assert root <= self.num_ranks - 1
assert self.rank <= self.num_ranks - 1
num_devices = common.cuda_call(cudart.cudaGetDeviceCount())
assert num_devices >= self.num_ranks
common.cuda_call(cudart.cudaSetDevice(self.rank))
if self.rank == root:
nccl_comm_id = nccl.get_unique_id()
else:
nccl_comm_id = None
nccl_comm_id = self.mpi_comm.bcast(nccl_comm_id, root=root)
self.nccl_comm = nccl.Communicator.init(nranks=self.num_ranks, rank=self.rank, unique_id=nccl_comm_id)
def setup(self, actual_input_shape, output_shape, root=0):
"""
Set up the multi-device environment and build/load the engine
Args:
root: Root rank for communication
"""
self.setup_multidevice(root)
# Load or build TRT engine
if self.rank == root:
engine_bin = bytes(self.build_serialized_network())
else:
engine_bin = None
# Broadcast the serialized engine from root to all ranks
engine_bin = self.mpi_comm.bcast(engine_bin, root=root)
# Deserialize the engine
self.engine = trt.Runtime(self.logger).deserialize_cuda_engine(engine_bin)
if self.engine is None:
print(f"[Rank {self.rank}] Failed deserializing engine!")
exit(-1)
print(f"[Rank {self.rank}] Succeeded deserializing engine!")
# Create an execution context
self.context = self.engine.create_execution_context()
# Set the NCCL communicator for multi-device communication
capsule = communicator_to_capsule(self.nccl_comm)
if not self.context.set_communicator(capsule):
print(f"[Rank {self.rank}] Failed to set communicator")
exit(-1)
# For dynamic shapes, we need to specify the actual input shape we want to use
input_name = self.engine.get_tensor_name(0)
self.context.set_input_shape(input_name, actual_input_shape)
# Allocate buffers for local portion of data
self.inputs, self.outputs, self.bindings = allocate_buffers(
self.engine, profile_idx=0, output_shape=output_shape
)
num_io = self.engine.num_io_tensors
tensor_names = [self.engine.get_tensor_name(i) for i in range(num_io)]
for i in range(num_io):
self.context.set_tensor_address(tensor_names[i], self.bindings[i])
def generate_random_input(sequence_length, batch_size):
"""Generate random float16 input data with the given shape."""
torch.manual_seed(42)
torch_input = torch.rand((sequence_length, batch_size, 4096)).to(torch.float16)
input_data = np.ascontiguousarray(torch_input.cpu().numpy())
return input_data, (sequence_length, batch_size, 4096)
def parse_args():
parser = argparse.ArgumentParser(description="Sample script for Attention MDTRT")
parser.add_argument("--onnx-path", type=str, required=True, help="Path to ONNX model")
parser.add_argument("--sequence-length", type=int, default=56320, help="Sequence length for input")
parser.add_argument("--batch-size", type=int, default=1, help="Batch size")
parser.add_argument("--num-iterations", type=int, default=50, help="Number of inference iterations for timing")
parser.add_argument("--save-output", type=str, default=None, help="Save output tensor to .npy file (root rank only)")
return parser.parse_args()
def main():
args = parse_args()
# Initialize MPI if available
if MPI is not None:
mpi_comm = MPI.COMM_WORLD
num_ranks = mpi_comm.Get_size()
rank = mpi_comm.Get_rank()
root = 0
else:
# Fallback for single-process execution
mpi_comm = None
num_ranks = 1
rank = 0
root = 0
actual_input_shape = (args.sequence_length, args.batch_size, 4096)
output_shape = (args.sequence_length, args.batch_size, 4096)
# Print configuration
if rank == root:
print(f"[setup] Configuration:")
print(f"[setup] Number of GPUs: {num_ranks}")
print(f"[setup] Sequence Length: {args.sequence_length}")
print(f"[setup] Batch Size: {args.batch_size}")
print(f"[setup] Data Type: float16")
print(f"[setup] Input Shape: {actual_input_shape}")
print(f"[setup] Output Shape: {output_shape}")
# Generate random input data with FULL sequence length (only on root rank)
if rank == root:
input_data, input_shape = generate_random_input(args.sequence_length, args.batch_size)
print(f"[Rank {rank}] Generated random input data with shape: {input_shape}")
if num_ranks == 1:
print(f"[Rank {rank}] Running single-device inference...")
try:
attention_sd = AttentionSD(mpi_comm, rank, args.onnx_path)
attention_sd.setup(actual_input_shape, output_shape)
sd_output = attention_sd.infer(input_data, output_shape, args.num_iterations)[0]
print(f"[Rank {rank}] Single-device inference completed")
print(f"[Rank {rank}] Output shape: {sd_output.shape}")
if args.save_output:
np.save(args.save_output, sd_output.float().cpu().numpy())
print(f"[Rank {rank}] Output saved to {args.save_output}")
attention_sd.cleanup()
except Exception as e:
print(f"[Rank {rank}] Error in single-device inference: {e}")
sys.exit(1)
else:
input_data = None
# Broadcast full input data to all ranks for multi-device inference
if MPI is not None and num_ranks > 1:
input_data = mpi_comm.bcast(input_data, root=root)
# Run multi-device inference if num_gpus > 1
if num_ranks > 1:
if MPI is None:
print(f"Error: MPI is required for multi-GPU tests but not available. Ensure you run with mpirun.")
sys.exit(1)
if nccl is None:
print(f"Error: nccl is required for multi-GPU tests but not available.")
sys.exit(1)
print(f"[Rank {rank}] Running multi-device inference...")
try:
attention_md = AttentionMD(mpi_comm, num_ranks, rank, args.onnx_path)
attention_md.setup(actual_input_shape, output_shape, root)
md_output = attention_md.infer(input_data, output_shape, args.num_iterations)[0]
print(f"[Rank {rank}] Multi-device inference completed")
print(f"[Rank {rank}] Output shape: {md_output.shape}")
if rank == root and args.save_output:
np.save(args.save_output, md_output.float().cpu().numpy())
print(f"[Rank {rank}] Output saved to {args.save_output}")
attention_md.cleanup()
except Exception as e:
print(f"[Rank {rank}] Error in multi-device inference: {e}")
sys.exit(1)
print(f"[Rank {rank}] Test completed successfully!")
if __name__ == "__main__":
main()