492 lines
19 KiB
Python
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()
|