Skip to content
Closed
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions tensorrt_llm/_torch/autotuner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Any, Callable, Dict, List, Set, Tuple, Union

import torch
from cuda.bindings import driver

Comment thread
limin2021 marked this conversation as resolved.
from tensorrt_llm.bindings.internal.runtime import delay_kernel
from tensorrt_llm.logger import logger
Expand Down Expand Up @@ -525,10 +526,27 @@ def _profile_single_kernel(
to get an average execution time. Stream synchronization and delays
are used to ensure accurate timing.
"""

use_cold_l2 = True

if use_cold_l2:
tensor_lists, num_buffers = self._prepare_input_tensors_with_batches(
inputs)
buffer_idx = 0
else:
tensor_lists = [inputs]
num_buffers = 1
buffer_idx = 0

stream = torch.cuda.current_stream()
# warm up, no timing
# always use the last batch for warmup
for _ in range(self.warmup):
runner(inputs, tactic=tactic, **kwargs)
# runner(tensor_lists[-1], tactic=tactic, **kwargs)
runner(tensor_lists[buffer_idx % num_buffers],
tactic=tactic,
**kwargs)
buffer_idx += 1
stream.synchronize()

# Delay the profiled kernel launch to eliminate affects of host time overhead in profiling.
Expand All @@ -540,13 +558,16 @@ def _profile_single_kernel(

start.record(stream=stream)
for _ in range(self.repeat):
runner(inputs, tactic=tactic, **kwargs)
runner(tensor_lists[buffer_idx % num_buffers],
tactic=tactic,
**kwargs)
buffer_idx += 1
end.record(stream=stream)
stream.synchronize()

avg_time = start.elapsed_time(end) / self.repeat

shapes = self._get_input_sizes(inputs)
shapes = self._get_input_sizes(tensor_lists[-1])
logger.debug(
f"[Autotuner] Profiled runner={runner}, tactic={tactic}, shapes={shapes}: {avg_time:.6f}ms."
)
Expand Down Expand Up @@ -733,6 +754,31 @@ def _prepare_input_tensors(
tensors.append(tensor)
return tensors

def _prepare_input_tensors_with_batches(
self,
inputs: List[torch.Tensor],
) -> Tuple[List[List[torch.Tensor]], int]:
# TODO: only consider tensor parameter?
one_buffer_bytes = sum(
input.numel() *
input.element_size() if isinstance(input, torch.Tensor) else 0
for input in inputs)
num_buffers = ceil(self._get_l2_cache_size_in_bytes() /
one_buffer_bytes)
num_buffers = min(num_buffers, self.repeat)

Comment thread
limin2021 marked this conversation as resolved.
inputs_list = [inputs]
# The last batch is for warmup
for _ in range(num_buffers - 1):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will increase the GPU memory a lot, by num_buffersx, can we clear L2 cache in another way? w/o increase the memory usage much.

This matters for TRTLLM because we need to use trace based method to record peak memory and estimate KV cache usage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But use_cold_l2 is opt in. If we don't setup this config, it means nothing is done for this mr. Still curious why some tests failed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better approach may be to have a L2 clearing kernel that runs between each iteration. I think you can do this by initialising an auxiliary buffer to maybe 2-3x the size of cache, and then launch a kernel to write random values between iterations (I think it needs to be random)

inputs_list.append(
list(t.clone() if isinstance(t, torch.Tensor) else t
for t in inputs))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not going to work for MOE, for MOE we have pointers referring to the weights in the workspace. We will need to call with do_preperation=True separately for every buffer.
This will need some changes to the preparation logic to support multiple internal workspaces too.
For this reason I think it might be better to go for an L2 clearing based approach that also reduces memory (see other comment)


logger.debug(
f"[Autotuner] To cold L2 cache, use {num_buffers} different tensors for profiling"
)
return inputs_list, num_buffers

def clear_cache(self) -> None:
"""Clear the profiling cache."""
self.profiling_cache.clear()
Expand All @@ -750,3 +796,32 @@ def print_profiling_cache(self):
runner_id, tactic, profile = value
logger.debug(
f"[Autotuner] {key}: (runner_id={runner_id}, tactic={tactic})")

def _get_l2_cache_size_in_bytes(self, device_id: int = 0) -> int:
device = self._checkCudaErrors(driver.cuDeviceGet(device_id))
return self._checkCudaErrors(
driver.cuDeviceGetAttribute(
driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_L2_CACHE_SIZE,
device,
))

def _checkCudaErrors(self, result) -> None:
if result[0].value:
raise RuntimeError("CUDA error code={}({})".format(
result[0].value, self._cudaGetErrorEnum(result[0])))
# CUDA APIs always return the status as the first element of the result tuple
if len(result) == 1:
return None
elif len(result) == 2:
return result[1]
else:
return result[1:]

Comment thread
limin2021 marked this conversation as resolved.
Outdated
def _cudaGetErrorEnum(error) -> str:
if isinstance(error, driver.CUresult):
err, name = driver.cuGetErrorName(error)
return name if err == driver.CUresult.CUDA_SUCCESS else "<unknown>"
elif isinstance(error, nvrtc.nvrtcResult):
return nvrtc.nvrtcGetErrorString(error)[1]
else:
raise RuntimeError("Unknown error type: {}".format(error))
Loading