chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:23:58 +08:00
commit 770d92cb1f
694 changed files with 114634 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
"""Compiler passes used in MLC LLM."""
from . import pipeline as _pipeline
@@ -0,0 +1,33 @@
"""The pass that attaches an empty function for initialization."""
import tvm
from tvm import IRModule, relax
@tvm.transform.module_pass(opt_level=0, name="AttachCUDAGraphAllocInitFunc")
class AttachCUDAGraphAllocInitFunc:
"""Attach an empty function for initialization."""
def __init__(self):
pass
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
bb = relax.BlockBuilder(mod)
alloc_func_gv = None
for gv, _ in mod.functions_items():
if gv.name_hint.startswith("cuda_graph_alloc"):
assert alloc_func_gv is None
alloc_func_gv = gv
if alloc_func_gv is None:
return mod
with bb.function("cuda_graph_alloc_init", []):
bb.emit_func_output(
relax.op.call_builtin_with_ctx(
"vm.builtin.cuda_graph.get_cached_alloc",
args=[alloc_func_gv, relax.prim_value(0)],
ty_args=relax.ObjectType(),
)
)
return bb.finalize()
@@ -0,0 +1,39 @@
"""The pass that attaches embedding allocation function to the IRModule."""
from typing import Any, Dict # noqa: UP035
import tvm
from tvm import IRModule, relax
@tvm.transform.module_pass(opt_level=0, name="AttachAllocEmbeddingTensorFunc")
class AttachAllocEmbeddingTensorFunc:
"""Attach embedding tensor allocation Relax function to IRModule."""
def __init__(self, metadata: Dict[str, Any]): # noqa: UP006
self.metadata = metadata
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
embed_func = None
for gv, func in mod.functions_items():
if gv.name_hint == "embed":
embed_func = func
if embed_func is None:
return mod
hidden_size = embed_func.ret_ty.shape[-1]
dtype = relax.DataTypeImm(embed_func.ret_ty.dtype.dtype)
bb = relax.BlockBuilder(mod)
with bb.function("alloc_embedding_tensor", []):
bb.emit_func_output(
bb.emit(
relax.op.builtin.alloc_tensor(
relax.ShapeExpr([self.metadata["prefill_chunk_size"], hidden_size]),
dtype,
runtime_device_index=0,
)
)
)
return bb.finalize()
@@ -0,0 +1,285 @@
"""The pass that attaches logit processor functions to the IRModule."""
import tvm
from tvm import IRModule
from tvm.script import tirx as T
from ..support.max_thread_check import (
check_thread_limits,
get_max_num_threads_per_block,
)
@tvm.transform.module_pass(opt_level=0, name="AttachLogitProcessFunc")
class AttachLogitProcessFunc:
"""Attach logit processing TIR functions to IRModule."""
def __init__(self, target: tvm.target.Target):
"""Initializer.
Parameters
----------
target : tvm.target.Target
The target of the model compilation.
"""
self.target = target
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
mod = mod.clone()
if self.target.kind.name == "llvm":
mod["apply_logit_bias_inplace"] = _get_apply_logit_bias_inplace_cpu()
mod["apply_penalty_inplace"] = _get_apply_penalty_inplace_cpu()
mod["apply_bitmask_inplace"] = _get_apply_bitmask_inplace_cpu()
else:
mod["apply_logit_bias_inplace"] = _get_apply_logit_bias_inplace(self.target)
mod["apply_penalty_inplace"] = _get_apply_penalty_inplace(self.target)
mod["apply_bitmask_inplace"] = _get_apply_bitmask_inplace(self.target)
return mod
def _get_apply_logit_bias_inplace_cpu():
@T.prim_func(s_tir=True)
def _apply_logit_bias_inplace(
var_logits: T.handle,
var_pos2seq_id: T.handle,
var_token_ids: T.handle,
var_logit_bias: T.handle,
) -> None:
"""Function that applies logit bias in place."""
T.func_attr(
{
"global_symbol": "apply_logit_bias_inplace",
"tirx.noalias": True,
"tirx.is_scheduled": True,
}
)
batch_size = T.int32()
vocab_size = T.int32()
num_token = T.int32()
logits = T.match_buffer(var_logits, (batch_size, vocab_size), "float32")
# seq_ids
pos2seq_id = T.match_buffer(var_pos2seq_id, (num_token,), "int32")
token_ids = T.match_buffer(var_token_ids, (num_token,), "int32")
logit_bias = T.match_buffer(var_logit_bias, (num_token,), "float32")
for i in range(num_token):
logits[pos2seq_id[i], token_ids[i]] += logit_bias[i]
return _apply_logit_bias_inplace
def _get_apply_logit_bias_inplace(target: tvm.target.Target):
tx = 1024 # default
max_num_threads_per_block = get_max_num_threads_per_block(target)
tx = min(tx, max_num_threads_per_block)
check_thread_limits(target, bdx=tx, bdy=1, bdz=1, gdz=1)
@T.prim_func(s_tir=True)
def _apply_logit_bias_inplace(
var_logits: T.handle,
var_pos2seq_id: T.handle,
var_token_ids: T.handle,
var_logit_bias: T.handle,
) -> None:
"""Function that applies logit bias in place."""
T.func_attr(
{
"global_symbol": "apply_logit_bias_inplace",
"tirx.noalias": True,
"tirx.is_scheduled": True,
}
)
batch_size = T.int32()
vocab_size = T.int32()
num_token = T.int32()
logits = T.match_buffer(var_logits, (batch_size, vocab_size), "float32")
# seq_ids
pos2seq_id = T.match_buffer(var_pos2seq_id, (num_token,), "int32")
token_ids = T.match_buffer(var_token_ids, (num_token,), "int32")
logit_bias = T.match_buffer(var_logit_bias, (num_token,), "float32")
for p0 in T.thread_binding(0, (num_token + tx - 1) // tx, "blockIdx.x"):
for p1 in T.thread_binding(0, tx, "threadIdx.x"):
with T.sblock("block"):
vp = T.axis.spatial(num_token, p0 * tx + p1)
T.where(p0 * tx + p1 < num_token)
logits[pos2seq_id[vp], token_ids[vp]] += logit_bias[vp]
return _apply_logit_bias_inplace
def _get_apply_penalty_inplace_cpu():
@T.prim_func(s_tir=True)
def _apply_penalty_inplace(
var_logits: T.handle,
var_seq_ids: T.handle,
var_pos2seq_id: T.handle,
var_token_ids: T.handle,
var_token_cnt: T.handle,
var_penalties: T.handle,
) -> None:
"""Function that applies penalties in place."""
T.func_attr(
{
"global_symbol": "apply_penalty_inplace",
"tirx.noalias": True,
"tirx.is_scheduled": True,
}
)
batch_size = T.int32()
vocab_size = T.int32()
num_token = T.int32()
num_seq = T.int32()
logits = T.match_buffer(var_logits, (batch_size, vocab_size), "float32")
seq_ids = T.match_buffer(var_seq_ids, (num_seq,), "int32")
pos2seq_id = T.match_buffer(var_pos2seq_id, (num_token,), "int32")
token_ids = T.match_buffer(var_token_ids, (num_token,), "int32")
token_cnt = T.match_buffer(var_token_cnt, (num_token,), "int32")
penalties = T.match_buffer(var_penalties, (num_seq, 3), "float32")
for token in T.serial(num_token):
with T.sblock("block"):
vp = T.axis.spatial(num_token, token)
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] -= (
penalties[pos2seq_id[vp], 0] + token_cnt[vp] * penalties[pos2seq_id[vp], 1]
)
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] = T.if_then_else(
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] < T.float32(0),
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] * penalties[pos2seq_id[vp], 2],
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] / penalties[pos2seq_id[vp], 2],
)
return _apply_penalty_inplace
def _get_apply_penalty_inplace(target: tvm.target.Target):
tx = 1024 # default
max_num_threads_per_block = get_max_num_threads_per_block(target)
tx = min(tx, max_num_threads_per_block)
check_thread_limits(target, bdx=tx, bdy=1, bdz=1, gdz=1)
@T.prim_func(s_tir=True)
def _apply_penalty_inplace(
var_logits: T.handle,
var_seq_ids: T.handle,
var_pos2seq_id: T.handle,
var_token_ids: T.handle,
var_token_cnt: T.handle,
var_penalties: T.handle,
) -> None:
"""Function that applies penalties in place."""
T.func_attr(
{
"global_symbol": "apply_penalty_inplace",
"tirx.noalias": True,
"tirx.is_scheduled": True,
}
)
batch_size = T.int32()
vocab_size = T.int32()
num_token = T.int32()
num_seq = T.int32()
logits = T.match_buffer(var_logits, (batch_size, vocab_size), "float32")
seq_ids = T.match_buffer(var_seq_ids, (num_seq,), "int32")
pos2seq_id = T.match_buffer(var_pos2seq_id, (num_token,), "int32")
token_ids = T.match_buffer(var_token_ids, (num_token,), "int32")
token_cnt = T.match_buffer(var_token_cnt, (num_token,), "int32")
penalties = T.match_buffer(var_penalties, (num_seq, 3), "float32")
for p0 in T.thread_binding(0, (num_token + tx - 1) // tx, "blockIdx.x"):
for p1 in T.thread_binding(0, tx, "threadIdx.x"):
with T.sblock("block"):
vp = T.axis.spatial(num_token, p0 * tx + p1)
T.where(p0 * tx + p1 < num_token)
# Penalties: (presence_penalty, frequency_penalty, repetition_penalty)
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] -= (
penalties[pos2seq_id[vp], 0] + token_cnt[vp] * penalties[pos2seq_id[vp], 1]
)
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] = T.if_then_else(
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]] < T.float32(0),
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]]
* penalties[pos2seq_id[vp], 2],
logits[seq_ids[pos2seq_id[vp]], token_ids[vp]]
/ penalties[pos2seq_id[vp], 2],
)
return _apply_penalty_inplace
def _get_apply_bitmask_inplace_cpu():
@T.prim_func(s_tir=True)
def _apply_bitmask_inplace(
var_logits: T.handle,
var_seq_ids: T.handle,
var_bitmask: T.handle,
) -> None:
"""Function that applies vocabulary masking in place."""
T.func_attr(
{
"global_symbol": "apply_bitmask_inplace",
"tirx.noalias": True,
"tirx.is_scheduled": True,
}
)
batch_size = T.int32()
vocab_size = T.int32()
num_seq = T.int32()
logits = T.match_buffer(var_logits, (batch_size, vocab_size), "float32")
seq_ids = T.match_buffer(var_seq_ids, (num_seq,), "int32")
bitmask = T.match_buffer(var_bitmask, (batch_size, (vocab_size + 31) // 32), "int32")
for token in T.serial(num_seq * vocab_size):
with T.sblock("block"):
vs = T.axis.spatial(num_seq, (token) // vocab_size)
vv = T.axis.spatial(vocab_size, (token) % vocab_size)
logits[seq_ids[vs], vv] = T.if_then_else(
(bitmask[seq_ids[vs], vv // 32] >> (vv % 32)) & 1 == 1,
logits[seq_ids[vs], vv],
T.min_value("float32"),
)
return _apply_bitmask_inplace
def _get_apply_bitmask_inplace(target: tvm.target.Target):
tx = 1024 # default
max_num_threads_per_block = get_max_num_threads_per_block(target)
tx = min(tx, max_num_threads_per_block)
check_thread_limits(target, bdx=tx, bdy=1, bdz=1, gdz=1)
@T.prim_func(s_tir=True)
def _apply_bitmask_inplace(
var_logits: T.handle,
var_seq_ids: T.handle,
var_bitmask: T.handle,
) -> None:
"""Function that applies vocabulary masking in place."""
T.func_attr(
{
"global_symbol": "apply_bitmask_inplace",
"tirx.noalias": True,
"tirx.is_scheduled": True,
}
)
batch_size = T.int32()
vocab_size = T.int32()
num_seq = T.int32()
logits = T.match_buffer(var_logits, (batch_size, vocab_size), "float32")
seq_ids = T.match_buffer(var_seq_ids, (num_seq,), "int32")
bitmask = T.match_buffer(var_bitmask, (batch_size, (vocab_size + 31) // 32), "int32")
for fused_s_v_0 in T.thread_binding(0, (num_seq * vocab_size + tx - 1) // tx, "blockIdx.x"):
for fused_s_v_1 in T.thread_binding(0, tx, "threadIdx.x"):
with T.sblock("block"):
vs = T.axis.spatial(num_seq, (fused_s_v_0 * tx + fused_s_v_1) // vocab_size)
vv = T.axis.spatial(vocab_size, (fused_s_v_0 * tx + fused_s_v_1) % vocab_size)
T.where(fused_s_v_0 * tx + fused_s_v_1 < num_seq * vocab_size)
logits[seq_ids[vs], vv] = T.if_then_else(
(bitmask[seq_ids[vs], vv // 32] >> (vv % 32)) & 1 == 1,
logits[seq_ids[vs], vv],
T.min_value("float32"),
)
return _apply_bitmask_inplace
@@ -0,0 +1,390 @@
"""The pass that attaches GPU sampler functions to the IRModule."""
from typing import Dict # noqa: UP035
import tvm
from tvm import IRModule, relax, te, tirx
from tvm.relax.frontend import nn
from tvm.script import tirx as T
from mlc_llm.op.batch_spec_verify import batch_spec_verify
from mlc_llm.op.top_p_pivot import top_p_pivot, top_p_renorm
@tvm.transform.module_pass(opt_level=0, name="AttachGPUSamplingFunc")
class AttachGPUSamplingFunc:
"""Attach GPU sampling functions to IRModule."""
def __init__(self, target: tvm.target.Target, variable_bounds: Dict[str, int]): # noqa: UP006
# Specifically for RWKV workloads, which contains -1 max_seq_len
max_batch_size = variable_bounds["batch_size"]
self.variable_bounds = {
"batch_size": max_batch_size,
"num_samples": max_batch_size,
"num_positions": 6 * max_batch_size,
}
self.non_negative_var = ["vocab_size"]
self.target = target
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
target_kind = self.target.kind.name
if target_kind not in ["cuda", "vulkan", "metal", "webgpu"]:
# Only enable GPU sampling for CUDA, Vulkan, Metal, and WebGPU.
return mod
bb = relax.BlockBuilder(mod)
if target_kind == "webgpu":
# Only attach functions that do not contain i8s for WebGPU
gv_names = [
gv.name_hint
for gv in [
_attach_argsort_func(bb),
_attach_sample_with_top_p(bb),
]
]
else:
gv_names = [
gv.name_hint
for gv in [
_attach_multinomial_sampling_func(bb),
_attach_argsort_func(bb),
_attach_sample_with_top_p(bb),
_attach_take_probs_func(bb),
_attach_batch_verifier(bb),
_attach_renormalize_by_top_p(bb, self.target),
]
]
mod = bb.finalize()
for gv_name in gv_names:
mod[gv_name] = (
mod[gv_name]
.with_attr("tir_var_upper_bound", self.variable_bounds)
.with_attr("tir_non_negative_var", self.non_negative_var)
)
return mod
def _attach_multinomial_sampling_func(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
num_samples = tirx.Var("num_samples", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
probs = relax.Var("probs", relax.TensorType((batch_size, vocab_size), "float32"))
uniform_samples = relax.Var("uniform_samples", relax.TensorType((num_samples,), "float32"))
sample_indices = relax.Var("sample_indices", relax.TensorType((num_samples,), "int32"))
with bb.function("multinomial_from_uniform", [probs, uniform_samples, sample_indices]):
with bb.dataflow():
sample_shape = relax.ShapeExpr([num_samples, 1])
probs_tensor = nn.wrap_nested(probs, name="probs")
uniform_samples_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
uniform_samples,
sample_shape,
ty_args=relax.TensorType(sample_shape, "float32"),
),
name="uniform_samples",
)
sample_indices_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
sample_indices,
sample_shape,
ty_args=relax.TensorType(sample_shape, "int32"),
),
name="sample_indices",
)
result_tensor = nn.multinomial_from_uniform(
probs_tensor,
uniform_samples_tensor,
sample_indices_tensor,
"int32",
name="nn_multinomial_from_uniform",
)
result = bb.emit(
relax.call_pure_packed(
"vm.builtin.reshape",
result_tensor._expr,
sample_indices.ty.shape,
ty_args=sample_indices.ty,
)
)
output = bb.emit_output(result)
gv = bb.emit_func_output(output)
return gv
def _attach_argsort_func(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
probs = relax.Var("probs", relax.TensorType((batch_size, vocab_size), "float32"))
with bb.function("argsort_probs", [probs]):
with bb.dataflow():
sorted_indices = bb.emit(relax.op.argsort(probs, descending=True, dtype="int32"))
sorted_values = bb.emit_te(
lambda unsorted_probs, sorted_indices: te.compute(
(batch_size, vocab_size),
lambda i, j: unsorted_probs[i, sorted_indices[i, j]],
name="take_sorted_probs",
),
probs,
sorted_indices,
primfunc_name_hint="take_sorted_probs",
)
output = bb.emit_output((sorted_values, sorted_indices))
gv = bb.emit_func_output(output)
return gv
@T.prim_func(s_tir=True)
def full(var_result: T.handle, value: T.int32):
"""The filling function for top k."""
batch_size = T.int32()
result = T.match_buffer(var_result, (batch_size, 1), "int32")
for i in T.serial(batch_size):
with T.sblock("block"):
vi = T.axis.spatial(batch_size, i)
result[vi, 0] = value
def _attach_sample_with_top_p(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
num_samples = tirx.Var("num_samples", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
sorted_probs = relax.Var("sorted_probs", relax.TensorType((batch_size, vocab_size), "float32"))
sorted_indices = relax.Var(
"sorted_indices", relax.TensorType((batch_size, vocab_size), "int32")
)
uniform_samples = relax.Var("uniform_samples", relax.TensorType((num_samples,), "float32"))
sample_indices = relax.Var("sample_indices", relax.TensorType((num_samples,), "int32"))
top_p = relax.Var("top_p", relax.TensorType((batch_size,), "float32"))
with bb.function(
"sample_with_top_p",
[sorted_probs, sorted_indices, uniform_samples, sample_indices, top_p],
):
with bb.dataflow():
sample_shape = relax.ShapeExpr([num_samples, 1])
top_p_shape = relax.ShapeExpr([batch_size, 1])
sorted_probs_tensor = nn.wrap_nested(sorted_probs, name="sorted_probs")
sorted_indices_tensor = nn.wrap_nested(sorted_indices, name="sorted_indices")
uniform_samples_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
uniform_samples,
sample_shape,
ty_args=relax.TensorType(sample_shape, "float32"),
),
name="uniform_samples",
)
sample_indices_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
sample_indices,
sample_shape,
ty_args=relax.TensorType(sample_shape, "int32"),
),
name="sample_indices",
)
top_p_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
top_p,
top_p_shape,
ty_args=relax.TensorType(top_p_shape, "float32"),
),
name="sample_indices",
)
top_k_tensor = nn.tensor_ir_op(
full,
name_hint="full",
args=[vocab_size],
out=nn.Tensor.placeholder(
[batch_size, 1],
"int32",
),
)
result_tensor = nn.sample_top_p_top_k_from_sorted_prob(
sorted_probs_tensor,
sorted_indices_tensor,
top_p_tensor,
top_k_tensor,
uniform_samples_tensor,
sample_indices_tensor,
)
result = bb.emit_output(
relax.call_pure_packed(
"vm.builtin.reshape",
result_tensor._expr,
sample_indices.ty.shape,
ty_args=sample_indices.ty,
)
)
gv = bb.emit_func_output(result)
return gv
def _attach_renormalize_by_top_p(bb: relax.BlockBuilder, target: tvm.target.Target):
batch_size = tirx.Var("batch_size", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
num_pivots = 3
probs = relax.Var("probs", relax.TensorType((batch_size, vocab_size), "float32"))
top_p = relax.Var("top_p", relax.TensorType((batch_size,), "float32"))
init_pivots = relax.Var("init_pivots", relax.TensorType((batch_size, num_pivots), "float32"))
with bb.function("renormalize_by_top_p", [probs, top_p, init_pivots]):
with bb.dataflow():
cutoff_output = bb.emit(
relax.call_tir(
bb.add_func(top_p_pivot(num_pivots, target), "top_p_pivot_cutoff"),
args=[probs, top_p, init_pivots],
out_ty=[top_p.ty, top_p.ty],
)
)
final_pivot = cutoff_output[0]
renorm_sum = cutoff_output[1]
renormalized_probs = bb.emit_output(
relax.call_tir(
bb.add_func(top_p_renorm(target), "top_p_renorm_after_cutoff"),
args=[probs, final_pivot, renorm_sum],
out_ty=probs.ty,
)
)
gv = bb.emit_func_output(renormalized_probs)
return gv
def _attach_take_probs_func(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
num_samples = tirx.Var("num_samples", "int64")
num_positions = tirx.Var("num_positions", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
unsorted_probs = relax.Var(
"unsorted_probs", relax.TensorType((batch_size, vocab_size), "float32")
)
sorted_indices = relax.Var(
"sorted_indices", relax.TensorType((batch_size, vocab_size), "int32")
)
sample_indices = relax.Var("sample_indices", relax.TensorType((num_samples,), "int32"))
sampling_results = relax.Var("sampling_result", relax.TensorType((num_samples,), "int32"))
top_prob_offsets = relax.Var("lobprob_offsets", relax.TensorType((num_positions,), "int32"))
@T.prim_func(s_tir=True)
def sampler_take_probs_tir(
var_unsorted_probs: T.handle,
var_sorted_indices: T.handle,
var_sample_indices: T.handle,
var_sampling_results: T.handle,
var_top_prob_offsets: T.handle,
var_sampled_values: T.handle,
var_top_prob_probs: T.handle,
var_top_prob_indices: T.handle,
):
batch_size = T.int32()
num_samples = T.int32()
num_positions = T.int32()
vocab_size = T.int32()
unsorted_probs = T.match_buffer(var_unsorted_probs, (batch_size, vocab_size), "float32")
sorted_indices = T.match_buffer(var_sorted_indices, (batch_size, vocab_size), "int32")
sample_indices = T.match_buffer(var_sample_indices, (num_samples,), "int32")
sampling_results = T.match_buffer(var_sampling_results, (num_samples,), "int32")
top_prob_offsets = T.match_buffer(var_top_prob_offsets, (num_positions,), "int32")
sampled_values = T.match_buffer(var_sampled_values, (num_samples,), "float32")
top_prob_probs = T.match_buffer(var_top_prob_probs, (num_positions,), "float32")
top_prob_indices = T.match_buffer(var_top_prob_indices, (num_positions,), "int32")
for i in T.serial(num_positions):
with T.sblock("top_prob"):
vi = T.axis.spatial(num_positions, i)
# Reads are data-dependent gathers; declare full-buffer read
# regions explicitly so tirx does not infer data-dependent regions.
T.reads(
top_prob_offsets[vi],
sorted_indices[0:batch_size, 0:vocab_size],
unsorted_probs[0:batch_size, 0:vocab_size],
)
T.writes(top_prob_indices[vi], top_prob_probs[vi])
row = T.floordiv(top_prob_offsets[vi], vocab_size)
col = T.floormod(top_prob_offsets[vi], vocab_size)
top_prob_indices[vi] = sorted_indices[row, col]
top_prob_probs[vi] = unsorted_probs[row, sorted_indices[row, col]]
for i in T.serial(num_samples):
with T.sblock("sample"):
vj = T.axis.spatial(num_samples, i)
T.reads(
sample_indices[vj],
sampling_results[vj],
unsorted_probs[0:batch_size, 0:vocab_size],
)
T.writes(sampled_values[vj])
sampled_values[vj] = unsorted_probs[sample_indices[vj], sampling_results[vj]]
args = [
unsorted_probs,
sorted_indices,
sample_indices,
sampling_results,
top_prob_offsets,
]
with bb.function("sampler_take_probs", args):
with bb.dataflow():
taken_probs_indices = bb.emit_output(
relax.call_tir(
bb.add_func(sampler_take_probs_tir, "sampler_take_probs_tir"),
args,
out_ty=[
relax.TensorType((num_samples,), "float32"),
relax.TensorType((num_positions,), "float32"),
relax.TensorType((num_positions,), "int32"),
],
)
)
gv = bb.emit_func_output(taken_probs_indices)
return gv
def _attach_batch_verifier(bb: relax.BlockBuilder):
num_nodes = tirx.Var("num_nodes", "int64")
nbatch = tirx.Var("nbatch", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
draft_probs = relax.Var("draft_probs", relax.TensorType((num_nodes, vocab_size), "float32"))
draft_tokens = relax.Var("draft_tokens", relax.TensorType((num_nodes,), "int32"))
model_probs = relax.Var("model_probs", relax.TensorType((num_nodes, vocab_size), "float32"))
token_tree_first_child = relax.Var(
"token_tree_first_child", relax.TensorType((num_nodes,), "int32")
)
token_tree_next_sibling = relax.Var(
"token_tree_next_sibling", relax.TensorType((num_nodes,), "int32")
)
uniform_samples = relax.Var("uniform_samples", relax.TensorType((num_nodes,), "float32"))
token_tree_parent_ptr = relax.Var("token_tree_parent_ptr", relax.TensorType((nbatch,), "int32"))
args = [
draft_probs,
draft_tokens,
model_probs,
token_tree_first_child,
token_tree_next_sibling,
uniform_samples,
token_tree_parent_ptr,
]
with bb.function("sampler_verify_draft_tokens", args):
with bb.dataflow():
res = bb.emit_output(
relax.call_tir_inplace(
bb.add_func(
batch_spec_verify(vocab_size),
"batch_verify_on_gpu_single_kernel",
),
args,
inplace_indices=[
args.index(model_probs),
args.index(token_tree_parent_ptr),
],
out_ty=[
model_probs.ty,
token_tree_parent_ptr.ty,
],
)
)
gv = bb.emit_func_output(res)
return gv
@@ -0,0 +1,274 @@
"""A compiler pass that attaches two-stage softmax with temperature."""
from typing import Any, Dict, Optional # noqa: UP035
import tvm
from tvm import relax, tirx
from tvm.ir.module import IRModule
from tvm.relax.expr_functor import PyExprMutator, mutator
from tvm.script import tirx as T
from ..support.max_thread_check import get_max_num_threads_per_block
@tvm.transform.module_pass(opt_level=0, name="AttachSoftmaxWithTemperature")
class AttachSoftmaxWithTemperature:
"""Rewrites one-shot softmax into two-stage softmax."""
def __init__(
self,
target: tvm.target.Target,
metadata: Optional[Dict[str, Any]] = None, # noqa: UP006
) -> None:
self.target = target
self.metadata = metadata
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
return _Rewriter(mod, self.target, self.metadata).transform()
@mutator
class _Rewriter(PyExprMutator):
def __init__(
self,
mod: IRModule,
target: tvm.target.Target,
metadata: Optional[Dict[str, Any]] = None, # noqa: UP006
) -> None:
super().__init__(mod)
self.mod = mod
self.target = target
self.metadata = metadata
self.chunk_size = 4096
self.active_vocab_size = self.metadata.get("active_vocab_size") if self.metadata else None
def transform(self) -> IRModule:
"""Entry point"""
batch_size = tirx.Var("batch_size", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
dtype = "float32"
logits = relax.Var("logits", relax.TensorType([batch_size, 1, vocab_size], dtype))
temperature = relax.Var("temperature", relax.TensorType([batch_size], dtype))
with self.builder_.function("softmax_with_temperature", params=[logits, temperature]):
with self.builder_.dataflow():
output_struct_info = logits.ty
new_shape = relax.ShapeExpr([batch_size, vocab_size])
logits = relax.call_pure_packed(
"vm.builtin.reshape",
logits,
new_shape,
ty_args=relax.TensorType(new_shape, dtype),
)
f_chunk_lse, f_softmax_with_lse = _get_lse_and_softmax_func(
self.target, self.chunk_size, self.active_vocab_size
)
chunked_result_struct_info = relax.TensorType(
(batch_size, (vocab_size + self.chunk_size - 1) // self.chunk_size),
"float32",
)
chunked_results = self.builder_.emit(
relax.call_tir(
self.builder_.add_func(f_chunk_lse, "chunk_lse"),
args=[logits, temperature],
out_ty=[
chunked_result_struct_info,
chunked_result_struct_info,
],
)
)
chunked_sum = chunked_results[0]
chunked_max = chunked_results[1]
softmax = self.builder_.emit(
relax.call_tir(
self.builder_.add_func(f_softmax_with_lse, "softmax_with_chunked_sum"),
args=[logits, temperature, chunked_sum, chunked_max],
out_ty=logits.ty,
)
)
softmax = self.builder_.emit_output(
relax.call_pure_packed(
"vm.builtin.reshape",
softmax,
output_struct_info.shape,
ty_args=output_struct_info,
)
)
self.builder_.emit_func_output(softmax)
return self.builder_.get()
def _get_lse_and_softmax_func(target: tvm.target.Target, chunk_size: int, active_vocab_size: int):
# NOTE: A quick note on the softmax implementation.
# We once tried to multiply every element by log2e which can be computed
# potentially more efficiently on hardware.
# However, when the input values are large, multiplying by the factor of log2e
# causes numerical issue in float32 dtype.
# This leads to the softmax output not summing up to 1.
# For numerical stability, we removed the log2e factor and switched back
# to the standard log/exp computation.
# The kernels below handle both the cases of temperature=0 and temperature != 0.
# - When temperature is not 0, the first kernel computes the log-sum-exp of
# chunks (subtracted by the max value in chunk), and the max values of chunks.
# The second kernel merges the log-sum-exp with the maximum values.
# - When temperature is 0, the first kernel computes the max value and the counts
# of the max value. The second kernel merges the max and counts, and set the
# softmax of the maximum values to "max_value / max_count".
@T.prim_func(s_tir=True)
def chunk_lse(
var_A: T.handle,
var_temperature: T.handle,
var_chunked_sum: T.handle,
var_chunked_max: T.handle,
):
T.func_attr({"tirx.noalias": True})
batch_size = T.int64()
vocab_size = T.int64()
num_chunks = T.int64()
A = T.match_buffer(var_A, (batch_size, vocab_size), dtype="float32")
temperature = T.match_buffer(var_temperature, (batch_size,), dtype="float32")
chunked_sum = T.match_buffer(var_chunked_sum, (batch_size, num_chunks), dtype="float32")
chunked_max = T.match_buffer(var_chunked_max, (batch_size, num_chunks), dtype="float32")
A_pad = T.sblock_alloc_buffer(
(batch_size, num_chunks, T.int64(chunk_size)), dtype="float32"
)
temp_max = T.sblock_alloc_buffer((batch_size, num_chunks), dtype="float32")
temp_sum = T.sblock_alloc_buffer((batch_size, num_chunks), dtype="float32")
for l0, l1, l2 in T.grid(batch_size, num_chunks, T.int64(chunk_size)):
with T.sblock("pad"):
v0, v1, v2 = T.axis.remap("SSS", [l0, l1, l2])
A_pad[v0, v1, v2] = T.Select(
v1 * T.int64(chunk_size) + v2
< (active_vocab_size if active_vocab_size is not None else vocab_size),
T.if_then_else(
temperature[v0] > T.float32(1e-5),
A[v0, v1 * T.int64(chunk_size) + v2] / temperature[v0],
A[v0, v1 * T.int64(chunk_size) + v2],
),
T.min_value("float32"),
)
for l0, l1, l2 in T.grid(batch_size, num_chunks, T.int64(chunk_size)):
with T.sblock("max"):
v0, v1, v2 = T.axis.remap("SSR", [l0, l1, l2])
with T.init():
temp_max[v0, v1] = T.min_value("float32")
temp_max[v0, v1] = T.max(temp_max[v0, v1], A_pad[v0, v1, v2])
for l0, l1, l2 in T.grid(batch_size, num_chunks, T.int64(chunk_size)):
with T.sblock("sum_exp"):
v0, v1, v2 = T.axis.remap("SSR", [l0, l1, l2])
with T.init():
temp_sum[v0, v1] = T.float32(0)
temp_sum[v0, v1] += T.if_then_else(
v1 * T.int64(chunk_size) + v2
< (active_vocab_size if active_vocab_size is not None else vocab_size),
T.Select(
temperature[v0] > T.float32(1e-5),
T.exp(A_pad[v0, v1, v2] - temp_max[v0, v1]),
T.cast(A_pad[v0, v1, v2] == temp_max[v0, v1], "float32"),
),
T.float32(0),
)
for l0, l1, l2 in T.grid(batch_size, num_chunks, T.int64(1)):
with T.sblock("log"):
v0, v1, v2 = T.axis.remap("SSS", [l0, l1, l2])
chunked_sum[v0, v1] = T.Select(
temperature[v0] > T.float32(1e-5),
T.log(temp_sum[v0, v1]),
temp_sum[v0, v1],
)
chunked_max[v0, v1] = temp_max[v0, v1]
@T.prim_func(s_tir=True)
def softmax_with_chunked_sum(
var_A: T.handle,
var_temperature: T.handle,
var_chunked_sum: T.handle,
var_chunked_max: T.handle,
var_softmax: T.handle,
):
T.func_attr({"tirx.noalias": True, "tirx.is_scheduled": 1})
batch_size = T.int64()
vocab_size = T.int64()
num_chunks = T.int64()
A = T.match_buffer(var_A, (batch_size, vocab_size), dtype="float32")
temperature = T.match_buffer(var_temperature, (batch_size,), dtype="float32")
chunked_sum = T.match_buffer(var_chunked_sum, (batch_size, num_chunks), dtype="float32")
chunked_max = T.match_buffer(var_chunked_max, (batch_size, num_chunks), dtype="float32")
softmax = T.match_buffer(var_softmax, (batch_size, vocab_size), dtype="float32")
temp_max = T.sblock_alloc_buffer((batch_size,), dtype="float32")
temp_sum = T.sblock_alloc_buffer((batch_size,), dtype="float32")
for l0, l1 in T.grid(batch_size, num_chunks):
with T.sblock("max"):
v0, v1 = T.axis.remap("SR", [l0, l1])
with T.init():
temp_max[v0] = T.min_value("float32")
temp_max[v0] = T.max(temp_max[v0], chunked_max[v0, v1])
for l0, l1 in T.grid(batch_size, num_chunks):
with T.sblock("sum_exp"):
v0, v1 = T.axis.remap("SR", [l0, l1])
with T.init():
temp_sum[v0] = T.float32(0)
temp_sum[v0] += T.Select(
temperature[v0] > T.float32(1e-5),
T.exp(chunked_sum[v0, v1] + chunked_max[v0, v1] - temp_max[v0]),
T.cast(chunked_max[v0, v1] == temp_max[v0], "float32") * chunked_sum[v0, v1],
)
for l0, l1, l2 in T.grid(batch_size, num_chunks, T.int64(chunk_size)):
with T.sblock("log_pad"):
v0, v1, v2 = T.axis.remap("SSS", [l0, l1, l2])
if v1 * T.int64(chunk_size) + v2 < vocab_size:
softmax[v0, v1 * T.int64(chunk_size) + v2] = T.Select(
v1 * T.int64(chunk_size) + v2
< (active_vocab_size if active_vocab_size is not None else vocab_size),
T.if_then_else(
temperature[v0] > T.float32(1e-5),
T.exp(
A[v0, v1 * T.int64(chunk_size) + v2] / temperature[v0]
- (T.log(temp_sum[v0]) + temp_max[v0])
),
T.cast(
A[v0, v1 * T.int64(chunk_size) + v2] == temp_max[v0],
"float32",
)
/ temp_sum[v0],
),
T.float32(0),
)
sch = tvm.s_tir.Schedule(IRModule({"softmax_with_chunked_sum": softmax_with_chunked_sum}))
def apply_gpu_schedule(target, sch):
max_threads = get_max_num_threads_per_block(target)
TX = 32
TY = max_threads // TX
unroll_depth = 64
sch.work_on("softmax_with_chunked_sum")
l0, l1, l2 = sch.get_loops("log_pad")
bx = sch.fuse(l0, l1)
sch.bind(bx, "blockIdx.x")
unroll, ty, tx = sch.split(l2, [None, TY, TX])
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
sch.annotate(unroll, ann_key="pragma_auto_unroll_max_step", ann_val=unroll_depth)
sch.annotate(unroll, ann_key="pragma_unroll_explicit", ann_val=1)
for block_name in ["sum_exp", "max"]:
block = sch.get_sblock(block_name)
sch.set_scope(block, buffer_index=0, storage_scope="shared")
sch.compute_at(block, bx)
r_loop = sch.get_loops(block)[-1]
r_loop, tx = sch.split(r_loop, [None, TX])
sch.reorder(tx, r_loop)
sch.bind(tx, "threadIdx.x")
sch.annotate(r_loop, ann_key="pragma_auto_unroll_max_step", ann_val=unroll_depth)
sch.annotate(r_loop, ann_key="pragma_unroll_explicit", ann_val=1)
return chunk_lse, sch.mod["softmax_with_chunked_sum"]
if target.kind.name == "llvm":
return chunk_lse, sch.mod["softmax_with_chunked_sum"]
return apply_gpu_schedule(target, sch)
@@ -0,0 +1,123 @@
"""The pass that attaches logit processor functions to the IRModule."""
import tvm
from tvm import IRModule, relax, tirx
from tvm.relax import BlockBuilder, TensorType
from tvm.script import tirx as T
@tvm.transform.module_pass(opt_level=0, name="AttachSpecDecodeAuxFuncs")
class AttachSpecDecodeAuxFuncs:
"""Attach logit processing TIR functions to IRModule."""
tensor_parallel_shards: int
def __init__(self, tensor_parallel_shards: int):
self.tensor_parallel_shards = tensor_parallel_shards
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
mod = mod.clone()
bb = BlockBuilder(mod)
bb.add_func(
_get_scatter_2d_inplace(dtype="float32", global_symbol="scatter_probs"),
"scatter_probs",
)
bb.add_func(
_get_gather_2d_inplace(dtype="float32", global_symbol="gather_probs"),
"gather_probs",
)
if "prefill_to_last_hidden_states" in mod:
hidden_states_struct_info = mod["prefill_to_last_hidden_states"].ret_ty.fields[0]
dtype = hidden_states_struct_info.dtype
_add_gather_hidden_states(bb, self.tensor_parallel_shards, dtype)
_add_scatter_hidden_states(bb, self.tensor_parallel_shards, dtype)
return bb.finalize()
def _get_scatter_2d_inplace(dtype: str, global_symbol: str):
@T.prim_func(s_tir=True)
def _scatter_2d(var_src: T.handle, var_indices: T.handle, var_dst: T.handle):
T.func_attr({"global_symbol": global_symbol, "tirx.noalias": True})
batch_size = T.int32()
m = T.int32()
n = T.int32()
src = T.match_buffer(var_src, (batch_size, n), dtype)
indices = T.match_buffer(var_indices, (batch_size,), "int32")
dst = T.match_buffer(var_dst, (m, n), dtype)
for b, j in T.grid(batch_size, n):
with T.sblock("scatter_2d"):
vb, vj = T.axis.remap("SS", [b, j])
dst[indices[vb], vj] = src[vb, vj]
return _scatter_2d
def _get_gather_2d_inplace(dtype: str, global_symbol: str):
@T.prim_func(s_tir=True)
def _gather_2d(var_src: T.handle, var_indices: T.handle, var_dst: T.handle):
T.func_attr({"global_symbol": global_symbol, "tirx.noalias": True})
batch_size = T.int32()
m = T.int32()
n = T.int32()
src = T.match_buffer(var_src, (m, n), dtype)
indices = T.match_buffer(var_indices, (batch_size,), "int32")
dst = T.match_buffer(var_dst, (batch_size, n), dtype)
for b, j in T.grid(batch_size, n):
with T.sblock("gather_2d"):
vb, vj = T.axis.remap("SS", [b, j])
dst[vb, vj] = src[indices[vb], vj]
return _gather_2d
def _add_scatter_hidden_states(bb: BlockBuilder, tensor_parallel_shards: int, dtype: str):
batch_size = tirx.Var("batch_size", "int64")
m = tirx.Var("m", "int64")
n = tirx.Var("n", "int64")
src = relax.Var("src", ty=TensorType([batch_size, n], dtype))
indices = relax.Var("indices", ty=TensorType([batch_size], "int32"))
dst = relax.Var("dst", ty=TensorType([m, n], dtype))
with bb.function("scatter_hidden_states", [src, indices, dst]):
with bb.dataflow():
if tensor_parallel_shards > 1:
indices = relax.op.ccl.broadcast_from_worker0(indices)
output = bb.emit_output(
relax.op.call_tir_inplace(
bb.add_func(
_get_scatter_2d_inplace(dtype, "_scatter_hidden_states"),
"_scatter_hidden_states",
),
[src, indices, dst],
2,
dst.ty,
)
)
gv = bb.emit_func_output(output)
return gv
def _add_gather_hidden_states(bb: BlockBuilder, tensor_parallel_shards: int, dtype: str):
batch_size = tirx.Var("batch_size", "int64")
m = tirx.Var("m", "int64")
n = tirx.Var("n", "int64")
src = relax.Var("src", ty=TensorType([m, n], dtype))
indices = relax.Var("indices", ty=TensorType([batch_size], "int32"))
dst = relax.Var("dst", ty=TensorType([batch_size, n], dtype))
with bb.function("gather_hidden_states", [src, indices, dst]):
with bb.dataflow():
if tensor_parallel_shards > 1:
indices = relax.op.ccl.broadcast_from_worker0(indices)
output = bb.emit_output(
relax.op.call_tir_inplace(
bb.add_func(
_get_gather_2d_inplace(dtype, "_gather_hidden_states"),
"_gather_hidden_states",
),
[src, indices, dst],
2,
dst.ty,
)
)
gv = bb.emit_func_output(output)
return gv
@@ -0,0 +1,154 @@
"""A couple of passes that simply supportive information onto the IRModule."""
from math import lcm
from typing import Any, Dict, List # noqa: UP035
import tvm
from tvm import IRModule, relax, tirx
from tvm.ir import Op
from tvm.relax.expr_functor import PyExprVisitor, visitor
@tvm.transform.module_pass(opt_level=0, name="AttachVariableBounds")
class AttachVariableBounds:
"""Attach variable bounds to each Relax function, which primarily helps with memory planning."""
def __init__(self, variable_bounds: Dict[str, int]): # noqa: UP006
# Specifically for RWKV workloads, which contains -1 max_seq_len
self.variable_bounds = {k: v for k, v in variable_bounds.items() if v > 0}
self.non_negative_var = ["vocab_size"]
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
for g_var, func in mod.functions_items():
if isinstance(func, relax.Function):
mod[g_var] = func.with_attr("tir_var_upper_bound", self.variable_bounds).with_attr(
"tir_non_negative_var", self.non_negative_var
)
return mod
@tvm.transform.module_pass(opt_level=0, name="AttachAdditionalPrimFuncs")
class AttachAdditionalPrimFuncs:
"""Attach extra TIR PrimFuncs to the IRModule"""
def __init__(self, functions: Dict[str, tirx.PrimFunc]): # noqa: UP006
self.functions = functions
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
for func_name, func in self.functions.items():
mod[func_name] = func.with_attr("global_symbol", func_name)
return mod
@tvm.transform.module_pass(opt_level=0, name="AttachMemoryPlanAttr")
class AttachMemoryPlanAttr:
"""Attach memory planning attribute for dynamic function output planning to Relax functions."""
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
for g_var, func in mod.functions_items():
if isinstance(func, relax.Function):
mod[g_var] = func.with_attr("relax.memory_plan_dynamic_func_output", True)
return mod
@tvm.transform.module_pass(opt_level=0, name="AttachCUDAGraphCaptureHints")
class AttachCUDAGraphSymbolicCaptureHints:
"""Attach CUDA graph capture hints to the IRModule"""
def __init__(self, hints: Dict[str, List[str]]): # noqa: UP006
self.hints = hints
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
for g_var, func in mod.functions_items():
func_name = g_var.name_hint
if isinstance(func, relax.Function):
if func_name in self.hints:
mod[g_var] = func.with_attr(
"relax.rewrite_cuda_graph.capture_symbolic_vars",
self.hints[func_name],
)
return mod
@tvm.transform.module_pass(opt_level=0, name="AttachPipelineParallelStages")
class AttachPipelineParallelStages:
"""Attach number of pipeline stages to relax functions."""
def __init__(self, pipeline_parallel_shards: int):
self.pipeline_parallel_shards = pipeline_parallel_shards
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
for g_var, func in mod.functions_items():
func_name = g_var.name_hint
if not isinstance(func, relax.Function) or func_name not in [
"prefill",
"decode",
"prefill_to_last_hidden_states",
"decode_to_last_hidden_states",
"batch_prefill",
"batch_decode",
"batch_verify",
"batch_prefill_to_last_hidden_states",
"batch_decode_to_last_hidden_states",
"batch_verify_to_last_hidden_states",
]:
continue
mod[g_var] = func.with_attr("pipeline_parallel_stages", self.pipeline_parallel_shards)
return mod
@tvm.transform.module_pass(opt_level=0, name="AttachSequenceLengthPaddingFactor")
class AttachSequenceLengthPaddingFactor:
"""Attach sequence length padding factor to the metadata"""
def __init__(self, target: tvm.target.Target, metadata: Dict[str, Any]): # noqa: UP006
self.target = target
self.metadata = metadata
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
@visitor
class _Visitor(PyExprVisitor):
def __init__(self, target: tvm.target.Target) -> None:
self.padding_factor = 1
self.target = target
self._op_call_dps_packed = Op.get("relax.call_dps_packed")
def run(self, mod: IRModule) -> int:
"""Entry point of the visitor."""
# Right now we only need padding for CUDA SM100a architecture.
# When the target is SM100a and uses cutlass gemm function,
# the sequence length needs to be padded to multiple of 4.
if self.target.kind.name != "cuda" or self.target.attrs.get("arch") != "sm_100a":
return 1
for _, func in mod.functions_items():
if isinstance(func, relax.Function):
self.visit_expr(func)
return self.padding_factor
def visit_call_(self, call: relax.Call) -> None:
super().visit_call_(call)
if call.op != self._op_call_dps_packed:
return
func_name = str(call.args[0].global_symbol)
if func_name in [
"cutlass.groupwise_scaled_gemm_e4m3fn_e4m3fn",
"cutlass.groupwise_scaled_bmm_e4m3fn_e4m3fn",
]:
# Find the minimum common multiple of padding factor and 4
self.padding_factor = lcm(self.padding_factor, 4)
# self.metadata["sequence_length_padding"] = True
padding_factor = _Visitor(self.target).run(mod)
if padding_factor > 1:
self.metadata["seqlen_padding_factor"] = padding_factor
return mod
@@ -0,0 +1,51 @@
"""A compiler pass that dispatches patterns to CUBLAS."""
import tvm
from tvm import IRModule, relax
from tvm.relax.backend import get_patterns_with_prefix
try:
import tvm.relax.backend.cuda.cublas as _cublas # noqa: F401
import tvm.relax.backend.rocm.hipblas as _hipblas # noqa: F401
except ImportError:
# Note: legacy path of cublas/hipblas for backward compatibility
pass
@tvm.transform.module_pass(opt_level=0, name="BLASDispatch")
class BLASDispatch:
"""A compiler pass that dispatches patterns to cuBLAS/hipBLAS."""
def __init__(self, target: tvm.target.Target) -> None:
if target.kind.name == "cuda":
self.has_blas = tvm.get_global_func("relax.ext.cublas", True)
if not self.has_blas:
raise Exception("cuBLAS is not enabled.")
self.patterns = get_patterns_with_prefix("cublas")
elif target.kind.name == "rocm":
self.has_blas = tvm.get_global_func("relax.ext.hipblas", True)
if not self.has_blas:
raise Exception("hipBLAS is not enabled.")
self.patterns = get_patterns_with_prefix("hipblas")
else:
raise Exception(f"Unsupported target {target.kind.name} for BLAS dispatch.")
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
model_names = [
gv.name_hint for gv, func in mod.functions.items() if isinstance(func, relax.Function)
]
# exclude single batch decode
model_names = [name for name in model_names if "batch" in name or "decode" not in name]
mod = tvm.transform.Sequential(
[
relax.transform.FuseOpsByPattern(
self.patterns,
bind_constants=False,
annotate_codegen=True,
entry_functions=model_names,
),
relax.transform.RunCodegen({}, entry_functions=model_names),
]
)(mod)
return mod
@@ -0,0 +1,31 @@
"""A compiler pass that cleans up undesired TIR attrs."""
from typing import List # noqa: UP035
import tvm
from tvm.ir.module import IRModule
@tvm.transform.module_pass(opt_level=0, name="CleanUpTIRAttrs")
class CleanUpTIRAttrs:
"""A compiler pass that cleans up undesired TIR attrs."""
def __init__(self, attrs: List[str]): # noqa: UP006
self.attrs = attrs
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
for g_var, func in mod.functions_items():
changed = False
for attr in self.attrs:
if func.attrs is not None and attr in func.attrs:
func = func.without_attr(attr)
changed = True
break
if changed:
mod[g_var] = func
return mod
@@ -0,0 +1,243 @@
"""A pass that rewrites KV cache creation functions in IRModule."""
import json
from typing import Any, Dict, List # noqa: UP035
import tvm
from tvm import IRModule, relax
from tvm.relax.frontend.nn.llm import kv_cache
from tvm.relax.frontend.nn.llm.kv_cache import RopeMode
from mlc_llm.support import logging
logger = logging.getLogger(__name__)
def extract_creation_args(func: relax.Function) -> Dict[str, Any]: # noqa: UP006
"""Extract the KV cache creation args from the given generic creation func."""
assert isinstance(func.body, relax.SeqExpr)
assert len(func.body.blocks) == 1
assert isinstance(func.body.blocks[0], relax.DataflowBlock)
assert isinstance(func.body.blocks[0].bindings[0], relax.VarBinding)
assert isinstance(func.body.blocks[0].bindings[0].value, relax.Call)
assert func.body.blocks[0].bindings[0].value.op == tvm.ir.Op.get("relax.call_pure_packed")
call_args = func.body.blocks[0].bindings[0].value.args
assert isinstance(call_args[0], relax.ExternFunc)
assert call_args[0].global_symbol == "mlc.create_paged_kv_cache_generic"
args = call_args[1:]
assert len(args) == 18
assert isinstance(args[0], (relax.StringImm, relax.Tuple))
# Check if attn_kind is a single value or a list with length of hidden layers
if isinstance(args[0], relax.StringImm):
assert args[0].value in ["mha", "mla"]
attn_kind = args[0].value
else:
assert len(args[0].fields) == args[3].value
for i, attention_type in enumerate(args[0].fields):
assert isinstance(attention_type, relax.StringImm)
assert attention_type.value in ["mha", "mla", "mha_sliding"]
attn_kind = [args[0].fields[i].value for i in range(len(args[0]))]
assert isinstance(args[1], relax.ShapeExpr)
assert len(args[1].values) == 5
assert isinstance(args[2], relax.ShapeExpr)
for i in range(3, 18):
if i in [13, 14, 17]:
continue
# PrimValue wrappers were phased out of Relax: scalar args are now bare
# tirx PrimExprs (IntImm/FloatImm) directly.
assert isinstance(args[i], (tvm.tirx.IntImm, tvm.tirx.FloatImm)), (
f"args[{i}] is {type(args[i])}"
)
assert isinstance(args[13], relax.StringImm)
assert isinstance(args[16], (relax.Constant, tvm.tirx.IntImm, tvm.tirx.FloatImm))
assert isinstance(args[17], relax.DataTypeImm)
return {
"attn_kind": attn_kind,
"max_batch_size": args[1].values[0],
"max_total_seq_len": args[1].values[1],
"prefill_chunk_size": args[1].values[2],
"page_size": args[1].values[3],
"support_sliding_window": args[1].values[4],
"layer_partition": args[2],
"num_hidden_layers": args[3].value,
"num_attention_heads": args[4].value,
"num_key_value_heads": args[5].value,
"qk_head_dim": args[6].value,
"v_head_dim": args[7].value,
"mla_original_qk_head_dim": args[8].value,
"mla_original_v_head_dim": args[9].value,
"rope_mode": args[10].value,
"rope_scale": args[11].value,
"rope_theta": args[12].value,
"rope_scaling": json.loads(args[13].value),
"rope_ext_factors": args[14],
"rotary_dim": args[15].value,
"enable_disaggregation": bool(args[16].value),
"dtype": args[17].value,
}
@tvm.transform.module_pass(opt_level=0, name="DispatchKVCacheCreation")
class DispatchKVCacheCreation:
"""Rewrite KV cache creation functions to IRModule."""
def __init__(
self,
target: tvm.target.Target,
flashinfer: bool,
metadata: Dict[str, Any], # noqa: UP006
) -> None:
"""Initializer.
Parameters
----------
target : tvm.target.Target
The target of the model compilation.
flashinfer : bool
A boolean indicating if flashinfer is enabled.
metadata : Dict[str, Any]
The model's metadata for KV cache creation.
Note that the metadata will be updated in this pass -- the
KV cache metadata will be attached.
"""
self.target = target
self.flashinfer = flashinfer
self.metadata = metadata
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
func_dict = {}
creation_func = None
for g_var, func in mod.functions_items():
# Try to find the `create_paged_kv_cache` func.
if g_var.name_hint == "create_paged_kv_cache":
creation_func = func
else:
func_dict[g_var] = func
if creation_func is None:
return mod
new_mod = IRModule(func_dict)
if mod.attrs is not None:
new_mod = new_mod.with_attrs(mod.attrs)
kwargs = extract_creation_args(creation_func)
self.attach_kv_cache_metadata(kwargs)
bb = relax.BlockBuilder(new_mod)
extern_mods = []
extern_mods += self.create_tir_paged_kv_cache(bb, kwargs)
extern_mods += self.create_flashinfer_paged_kv_cache(bb, kwargs)
mod = bb.finalize()
mod_attrs = dict(mod.attrs) if mod.attrs else {}
mod = mod.with_attr("external_mods", mod_attrs.get("external_mods", []) + extern_mods)
return mod
def attach_kv_cache_metadata(self, kwargs: Dict[str, Any]): # noqa: UP006
"""Attach the KV cache metadata to model metadata."""
self.metadata["kv_cache"] = {
"num_hidden_layers": kwargs["num_hidden_layers"],
"num_attention_heads": kwargs["num_attention_heads"],
"num_key_value_heads": kwargs["num_key_value_heads"],
"head_dim": kwargs["qk_head_dim"],
}
def create_tir_paged_kv_cache(
self,
bb: relax.BlockBuilder,
kwargs: Dict[str, Any], # noqa: UP006
) -> List[tvm.runtime.Module]: # noqa: UP006
"""Create the TIR-based PagedKVCache"""
max_batch_size = relax.Var("max_batch_size_", relax.ShapeType([kwargs["max_batch_size"]]))
max_total_seq_len = relax.Var(
"max_total_seq_len_", relax.ShapeType([kwargs["max_total_seq_len"]])
)
prefill_chunk_size = relax.Var(
"prefill_chunk_size_", relax.ShapeType([kwargs["prefill_chunk_size"]])
)
page_size = relax.Var("page_size_", relax.ShapeType([kwargs["page_size"]]))
support_sliding_window = relax.Var(
"support_sliding_window_",
relax.ShapeType([kwargs["support_sliding_window"]]),
)
# Ensure 'enable_disaggregation' is optional
enable_disaggregation = kwargs.pop("enable_disaggregation", False)
kwargs["enable_disaggregation"] = enable_disaggregation
with bb.function(
name="create_tir_paged_kv_cache",
params=[
max_batch_size,
max_total_seq_len,
prefill_chunk_size,
page_size,
support_sliding_window,
],
):
cache = kv_cache.TIRPagedKVCache(target=self.target, **kwargs)
bb.emit_func_output(cache._expr)
return cache.extern_mods
def create_flashinfer_paged_kv_cache(
self,
bb: relax.BlockBuilder,
kwargs: Dict[str, Any], # noqa: UP006
) -> List[tvm.runtime.Module]: # noqa: UP006
"""Create the FlashInfer-based PagedKVCache"""
# Filter the cases which FlashInfer does not support.
if (
not self.flashinfer
or self.target.kind.name != "cuda"
or str(kwargs["dtype"]) not in ["float16", "bfloat16"]
or (
kwargs["rope_mode"] == RopeMode.INLINE
and (
kwargs["rotary_dim"] != kwargs["qk_head_dim"]
or kwargs["qk_head_dim"] != kwargs["v_head_dim"]
)
)
):
return []
max_batch_size = relax.Var("max_batch_size_", relax.ShapeType([kwargs["max_batch_size"]]))
max_total_seq_len = relax.Var(
"max_total_seq_len_", relax.ShapeType([kwargs["max_total_seq_len"]])
)
prefill_chunk_size = relax.Var(
"prefill_chunk_size_", relax.ShapeType([kwargs["prefill_chunk_size"]])
)
page_size = relax.Var("page_size_", relax.ShapeType([kwargs["page_size"]]))
support_sliding_window = relax.Var(
"support_sliding_window_",
relax.ShapeType([kwargs["support_sliding_window"]]),
)
try:
with bb.function(
name="create_flashinfer_paged_kv_cache",
params=[
max_batch_size,
max_total_seq_len,
prefill_chunk_size,
page_size,
support_sliding_window,
],
):
cache = kv_cache.FlashInferPagedKVCache(target=self.target, **kwargs)
bb.emit_func_output(cache._expr)
except Exception as e:
logger.info(
"Error caught when creating FlashInfer PagedKVCache: %s\n"
"The model will fallback to TIR-based KV cache.",
e,
)
return []
return cache.extern_mods
@@ -0,0 +1,176 @@
"""A pass that dispatch generic calls of triton kernels to specific kernel implementations."""
from typing import List # noqa: UP035
import tvm
from tvm import IRModule, relax
from tvm.relax.expr_functor import PyExprMutator, mutator
from mlc_llm.op.triton import (
get_tir_w8a8_block_fp8_group_matmul,
get_tir_w8a8_block_fp8_matmul,
)
from mlc_llm.support import logging
logger = logging.getLogger(__name__)
@mutator
class _Rewriter(PyExprMutator):
def __init__(self, mod: IRModule, target: tvm.target.Target) -> None:
super().__init__(mod)
self.mod = mod
self.target = target
self.extern_mods: List[tvm.runtime.Module] = [] # noqa: UP006
def transform(self) -> tvm.IRModule:
"""Entry point of the transformation"""
for g_var, func in self.mod.functions_items():
if not isinstance(func, relax.Function):
continue
new_func = self.visit_expr(func)
# new_func = remove_all_unused(new_func)
self.builder_.update_func(g_var, new_func)
mod = self.builder_.finalize()
mod_attrs = dict(mod.attrs) if mod.attrs else {}
mod = mod.with_attr(
"external_mods", list(mod_attrs.get("external_mods", [])) + self.extern_mods
)
return mod
def visit_call_(self, call: relax.Call) -> relax.Expr:
call = super().visit_call_(call)
if (
call.op != tvm.ir.Op.get("relax.call_dps_packed")
or not isinstance(call.args[0], relax.ExternFunc)
or not str(call.args[0].global_symbol).startswith("mlc.triton.")
):
return call
global_symbol = str(call.args[0].global_symbol)
assert isinstance(call.args[1], relax.Tuple)
if global_symbol == "mlc.triton.w8a8_block_fp8_matmul":
return self.w8a8_block_fp8_matmul(call.args[1].fields, call.ty)
if global_symbol == "mlc.triton.w8a8_block_fp8_group_matmul":
return self.w8a8_block_fp8_group_matmul(call.args[1].fields, call.ty)
raise ValueError(f"Unknown mlc.triton kernel identifier: {global_symbol}")
def w8a8_block_fp8_matmul(
self,
args: List[relax.Expr], # noqa: UP006
out_ty: relax.Type,
) -> relax.Expr:
"""Emit the w8a8_block_fp8_matmul triton kernel."""
assert len(args) == 16
x, weight, x_scale, weight_scale = args[:4]
(
N,
K,
block_n,
block_k,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
BLOCK_SIZE_K,
GROUP_SIZE_M,
num_warps,
num_stages,
) = [arg.value.value for arg in args[4:14]]
in_dtype, out_dtype = str(args[14].value), str(args[15].value)
prim_func, func_name = get_tir_w8a8_block_fp8_matmul(
N,
K,
block_n,
block_k,
in_dtype,
out_dtype,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
BLOCK_SIZE_K,
GROUP_SIZE_M,
num_warps,
num_stages,
self.extern_mods,
)
if prim_func is None:
# The TIR function is already in the IRModule
gv = self.builder_.get().get_global_var(func_name)
else:
# Add the TIR function to the IRModule
gv = self.builder_.add_func(prim_func, func_name)
return relax.call_tir(gv, [x, weight, x_scale, weight_scale], out_ty=out_ty)
def w8a8_block_fp8_group_matmul(
self,
args: List[relax.Expr], # noqa: UP006
out_ty: relax.Type,
) -> relax.Expr:
"""Emit the w8a8_block_fp8_group_matmul triton kernel."""
assert len(args) == 19
x, weight, x_scale, weight_scale, expert_ids, indptr = args[:6]
(
N,
K,
num_experts,
block_n,
block_k,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
BLOCK_SIZE_K,
GROUP_SIZE_M,
num_warps,
num_stages,
) = [arg.value.value for arg in args[6:17]]
in_dtype, out_dtype = str(args[17].value), str(args[18].value)
prim_func, func_name = get_tir_w8a8_block_fp8_group_matmul(
N,
K,
num_experts,
block_n,
block_k,
in_dtype,
out_dtype,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
BLOCK_SIZE_K,
GROUP_SIZE_M,
num_warps,
num_stages,
self.extern_mods,
)
if prim_func is None:
# The TIR function is already in the IRModule
gv = self.builder_.get().get_global_var(func_name)
else:
# Add the TIR function to the IRModule
gv = self.builder_.add_func(prim_func, func_name)
return relax.call_tir(
gv,
[x, weight, x_scale, weight_scale, expert_ids, indptr],
out_ty=out_ty,
)
@tvm.transform.module_pass(opt_level=0, name="DispatchTritonKernel")
class DispatchTritonKernel:
"""Rewrite KV cache creation functions to IRModule."""
def __init__(self, target: tvm.target.Target) -> None:
"""Initializer.
Parameters
----------
"""
self.target = target
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
if self.target.kind.name != "cuda":
return mod
return _Rewriter(mod, self.target).transform()
@@ -0,0 +1,87 @@
"""Memory usage estimation analysis function for Relax functions."""
import json
from typing import Any, Dict # noqa: UP035
import tvm
from tvm import relax, tirx
from tvm.ir import IRModule, Op
from tvm.relax.expr_functor import PyExprVisitor, visitor
from mlc_llm.support import logging
logger = logging.getLogger(__name__)
@tvm.transform.module_pass(opt_level=0, name="AttachMetadata")
class AttachMetadataWithMemoryUsage:
"""Attach a Relax function that returns metadata in a JSON string"""
def __init__(self, metadata: Dict[str, Any]): # noqa: UP006
self.metadata = metadata
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
func_name = "_metadata"
def _emit_metadata(metadata):
bb = relax.BlockBuilder()
with bb.function(func_name, params=[]):
bb.emit_func_output(relax.StringImm(json.dumps(metadata)))
return bb.finalize()[func_name]
self.metadata["memory_usage"] = _MemoryEstimator().run(mod)
mod[func_name] = _emit_metadata(self.metadata)
return mod
@visitor
class _MemoryEstimator(PyExprVisitor):
"""The IR visitor which estimates the memory usage of each Relax function."""
def __init__(self) -> None:
self.planned_alloc_mem = 0
self.planned_mem_num = 0
self._op_alloc_tensor = Op.get("relax.builtin.alloc_tensor")
self._op_alloc_storage = Op.get("relax.memory.alloc_storage")
def run(self, mod: IRModule) -> Dict[str, int]: # noqa: UP006
"""Entry point of the visitor."""
result: Dict[str, int] = {} # noqa: UP006
for global_var, func in mod.functions_items():
if isinstance(func, relax.Function):
self.planned_alloc_mem = 0
self.planned_mem_num = 0
self.visit_expr(func)
result[global_var.name_hint] = self.planned_alloc_mem
logger.info(
"[Memory usage] Function `%s`: %.2f MB",
global_var.name_hint,
self.planned_alloc_mem / 1024 / 1024,
)
return result
def visit_call_(self, call: relax.Call) -> None:
if call.op == self._op_alloc_tensor:
self._builtin_tensor_alloc(shape=call.args[0], dtype_str=call.args[1].value)
elif call.op == self._op_alloc_storage:
self._storage_alloc(size=call.args[0])
super().visit_call_(call)
def _builtin_tensor_alloc(self, shape: relax.Expr, dtype_str: str) -> None:
assert isinstance(shape, relax.ShapeExpr)
size = 1
for dim_len in shape.values:
if not isinstance(dim_len, tvm.tirx.IntImm):
return
size *= dim_len.value
dtype = tvm.DataType(dtype_str)
self.planned_mem_num += 1
self.planned_alloc_mem += size * ((dtype.bits + 7) // 8) * dtype.lanes
def _storage_alloc(self, size: relax.Expr) -> None:
assert isinstance(size, relax.ShapeExpr)
if isinstance(size.values[0], tirx.IntImm):
self.planned_mem_num += 1
self.planned_alloc_mem += size.values[0].value
@@ -0,0 +1,238 @@
"""A compiler pass that fuses add + rms_norm."""
from typing import Optional
import tvm
from tvm import relax
from tvm.relax.analysis import remove_all_unused
from tvm.relax.expr_functor import PyExprMutator, mutator
from tvm.script import tirx as T
from ..support.max_thread_check import get_max_num_threads_per_block
def _get_add_rms_norm_decode(hidden_size: int, eps: float, TX: int, in_dtype: str):
if in_dtype not in ("float16", "bfloat16"):
raise ValueError(f"Unsupported data type: {in_dtype}")
inv_hidden_size = T.float32(1.0 / float(hidden_size))
eps = T.float32(eps)
add_local_size = hidden_size // TX
@T.prim_func(private=True, s_tir=True)
def decode_add_rms(pA: T.handle, pB: T.handle, pC: T.handle, pO: T.handle, pAdd: T.handle):
T.func_attr({"tirx.noalias": True, "tirx.is_scheduled": 1})
batch_size = T.int32()
A = T.match_buffer(pA, (batch_size, 1, hidden_size), in_dtype)
B = T.match_buffer(pB, (batch_size, 1, hidden_size), in_dtype)
C = T.match_buffer(pC, (hidden_size,), in_dtype)
out = T.match_buffer(pO, (batch_size, 1, hidden_size), in_dtype)
add = T.match_buffer(pAdd, (batch_size, 1, hidden_size), in_dtype)
add_local = T.sblock_alloc_buffer((hidden_size // TX,), in_dtype, scope="local")
sum_shared = T.sblock_alloc_buffer((batch_size, 1), scope="shared")
sum_local = T.sblock_alloc_buffer((TX, batch_size, 1), scope="local")
for v_bx in T.thread_binding(batch_size, thread="blockIdx.x"):
for v_tx in T.thread_binding(
TX,
thread="threadIdx.x",
annotations={
"pragma_auto_unroll_max_step": 256,
"pragma_unroll_explicit": 1,
},
):
for i in range(add_local_size):
with T.sblock("T_add"):
bx = T.axis.spatial(batch_size, v_bx)
h = T.axis.spatial(hidden_size, i * TX + v_tx)
add_local[h // TX] = A[bx, 0, h] + B[bx, 0, h]
with T.sblock("T_write_back"):
bx = T.axis.spatial(batch_size, v_bx)
v_ax1 = T.axis.spatial(1, 0)
h = T.axis.spatial(hidden_size, i * TX + v_tx)
add[bx, v_ax1, h] = add_local[h // TX]
with T.sblock("T_multiply_red_rf_init"):
tx, bx = T.axis.remap("SS", [v_tx, v_bx])
sum_local[tx, bx, 0] = T.float32(0)
for v_i, _j in T.grid(add_local_size, 1):
with T.sblock("T_multiply_red_rf_update"):
tx, bx, i = T.axis.remap("SSR", [v_tx, v_bx, v_i])
sum_local[tx, bx, 0] += T.float32(add_local[i]) * T.float32(add_local[i])
for _j in range(1):
for v_tx_2 in T.thread_binding(TX, thread="threadIdx.x"):
with T.sblock("T_multiply_red"):
tx, bx = T.axis.remap("RS", [v_tx_2, v_bx])
T.reads(sum_local[tx, bx, 0])
T.writes(sum_shared[bx, 0])
with T.init():
sum_shared[bx, 0] = T.float32(0)
sum_shared[bx, 0] += sum_local[tx, bx, 0]
for i in range(add_local_size):
for v_tx_2 in T.thread_binding(TX, thread="threadIdx.x"):
with T.sblock("T_cast_2"):
bx = T.axis.spatial(batch_size, v_bx)
h = T.axis.spatial(hidden_size, i * TX + v_tx_2)
out[bx, 0, h] = T.cast(
T.rsqrt(sum_shared[bx, 0] * inv_hidden_size + eps)
* T.float32(add_local[h // TX])
* T.float32(C[h]),
dtype=in_dtype,
)
return decode_add_rms
def _get_add_rms_norm_prefill(hidden_size: int, eps: float, TX: int, in_dtype: str):
if in_dtype not in ("float16", "bfloat16"):
raise ValueError(f"Unsupported data type: {in_dtype}")
inv_hidden_size = T.float32(1.0 / float(hidden_size))
eps = T.float32(eps)
add_local_size = hidden_size // TX
@T.prim_func(private=True, s_tir=True)
def prefill_add_rms(pA: T.handle, pB: T.handle, pC: T.handle, pO: T.handle, pAdd: T.handle):
T.func_attr({"tirx.noalias": True, "tirx.is_scheduled": 1})
seq_len = T.int32()
A = T.match_buffer(pA, (1, seq_len, hidden_size), in_dtype)
B = T.match_buffer(pB, (1, seq_len, hidden_size), in_dtype)
C = T.match_buffer(pC, (hidden_size,), in_dtype)
out = T.match_buffer(pO, (1, seq_len, hidden_size), in_dtype)
add = T.match_buffer(pAdd, (1, seq_len, hidden_size), in_dtype)
add_local = T.sblock_alloc_buffer((hidden_size // TX,), in_dtype, scope="local")
sum_shared = T.sblock_alloc_buffer((1, seq_len), scope="shared")
sum_local = T.sblock_alloc_buffer((TX, 1, seq_len), scope="local")
for v_bx in T.thread_binding(seq_len, thread="blockIdx.x"):
for v_tx in T.thread_binding(
TX,
thread="threadIdx.x",
annotations={
"pragma_auto_unroll_max_step": 256,
"pragma_unroll_explicit": 1,
},
):
for v_i in range(add_local_size):
with T.sblock("T_add"):
bx = T.axis.spatial(seq_len, v_bx)
h = T.axis.spatial(hidden_size, v_i * TX + v_tx)
add_local[h // TX] = A[0, bx, h] + B[0, bx, h]
with T.sblock("T_write_back"):
bx = T.axis.spatial(seq_len, v_bx)
h = T.axis.spatial(hidden_size, v_i * TX + v_tx)
add[0, bx, h] = add_local[h // TX]
with T.sblock("T_multiply_red_rf_init"):
tx, bx = T.axis.remap("SS", [v_tx, v_bx])
sum_local[tx, 0, bx] = T.float32(0)
for v_i, _j in T.grid(add_local_size, 1):
with T.sblock("T_multiply_red_rf_update"):
tx, bx, i = T.axis.remap("SSR", [v_tx, v_bx, v_i])
sum_local[tx, 0, bx] += T.float32(add_local[i]) * T.float32(add_local[i])
for _j in range(1):
for v_tx_2 in T.thread_binding(TX, thread="threadIdx.x"):
with T.sblock("T_multiply_red"):
tx, bx = T.axis.remap("RS", [v_tx_2, v_bx])
with T.init():
sum_shared[0, bx] = T.float32(0)
sum_shared[0, bx] = sum_shared[0, bx] + sum_local[tx, 0, bx]
for v_i in range(add_local_size):
for v_tx_2 in T.thread_binding(TX, thread="threadIdx.x"):
with T.sblock("T_cast_2"):
bx = T.axis.spatial(seq_len, v_bx)
v1 = T.axis.spatial(hidden_size, v_i * TX + v_tx_2)
out[0, bx, v1] = T.cast(
T.rsqrt(sum_shared[0, bx] * inv_hidden_size + eps)
* T.float32(add_local[v1 // TX])
* T.float32(C[v1]),
dtype=in_dtype,
)
return prefill_add_rms
@tvm.transform.module_pass(opt_level=0, name="FuseAddRMSNorm")
class FuseAddRMSNorm:
"""A compiler pass that fuses add + rms_norm."""
def __init__(self, target: tvm.target.Target) -> None:
"""Initializer.
Parameters
----------
target : tvm.target.Target
Target device.
"""
self.target = target
def transform_module(self, mod: tvm.IRModule, _ctx: tvm.transform.PassContext) -> tvm.IRModule:
"""IRModule-level transformation."""
return _FuseAddRMSNormRewriter(mod.clone(), self.target).transform()
@mutator
class _FuseAddRMSNormRewriter(PyExprMutator):
def __init__(self, mod: tvm.IRModule, target: tvm.target.Target):
super().__init__(mod)
self.mod = mod
self.prefill_norm_gv: Optional[tvm.ir.GlobalVar] = None
self.decode_norm_gv: Optional[tvm.ir.GlobalVar] = None
self.TX = min(1024, get_max_num_threads_per_block(target))
def transform(self) -> tvm.IRModule:
"""Entry point of the transformation"""
for g_var, func in self.mod.functions_items():
if not isinstance(func, relax.Function):
continue
new_func = self.visit_expr(func)
new_func = remove_all_unused(new_func)
self.builder_.update_func(g_var, new_func)
return self.builder_.finalize()
def visit_call_(self, call: relax.Call) -> relax.Expr:
call = super().visit_call_(call)
# Match the "rms_norm(add(x1, x2), w)" pattern
if call.op != tvm.ir.Op.get("relax.nn.rms_norm") or call.ty.dtype not in [
"bfloat16",
"float16",
]:
return call
assert len(call.args) == 2
weight = call.args[1]
eps = call.attrs.epsilon
assert isinstance(call.args[0], relax.Var)
y = self.lookup_binding(call.args[0])
if not isinstance(y, relax.Call) or y.op != tvm.ir.Op.get("relax.add"):
return call
assert len(y.args) == 2
x1 = y.args[0]
x2 = y.args[1]
# Extra check
n, _, h = x1.ty.shape
h = int(h)
if h % self.TX != 0:
return call
is_prefill = n == 1
func_gv = self.prefill_norm_gv if is_prefill else self.decode_norm_gv
if func_gv is None:
if is_prefill:
func_gv = self.builder_.add_func(
_get_add_rms_norm_prefill(h, eps, self.TX, call.ty.dtype),
"fuse_add_norm_prefill",
)
self.prefill_norm_gv = func_gv
else:
func_gv = self.builder_.add_func(
_get_add_rms_norm_decode(h, eps, self.TX, call.ty.dtype),
"fuse_add_norm_decode",
)
self.decode_norm_gv = func_gv
tuple_output = self.builder_.emit(
relax.call_tir(
func_gv,
[x1, x2, weight],
out_ty=[x1.ty, x2.ty],
)
)
new_o = relax.TupleGetItem(tuple_output, 0)
new_y = self.builder_.emit(relax.TupleGetItem(tuple_output, 1))
self.set_var_remap(call.args[0], new_y)
return new_o
@@ -0,0 +1,85 @@
"""A compiler pass that fuses dequantize + matmul + elementwise."""
import tvm
from tvm import IRModule, relax
from tvm.relax.dpl.pattern import GlobalVarPattern, TuplePattern, is_op, wildcard
@tvm.transform.module_pass(opt_level=0, name="FuseDequantizeMatmulEwise")
class FuseDequantizeMatmulEwise:
"""A compiler pass that fuses dequantize + matmul + elementwise."""
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
seq = []
for n_aux_tensor in [0, 1, 2, 3, 4]:
for match_ewise in [0, 1, 2, 3, 6]:
if match_ewise == 6 and n_aux_tensor != 4:
continue
seq.append(
relax.transform.FuseOpsByPattern(
[
(
"dequantize_matmul",
*_pattern(match_ewise, n_aux_tensor),
)
]
)
)
seq.append(relax.transform.FuseTIR())
return tvm.transform.Sequential(seq)(mod)
def _pattern(match_ewise: int, n_aux_tensor: int):
w_scaled = wildcard()
x = wildcard()
w = is_op("relax.call_tir")(
GlobalVarPattern(),
TuplePattern([w_scaled] + [wildcard() for _ in range(n_aux_tensor)]),
add_constraint=False,
)
matmul = is_op("relax.call_tir")(
GlobalVarPattern(),
TuplePattern([x, w] + [wildcard() for _ in range(match_ewise)]),
add_constraint=False,
)
annotations = {
"w_scaled": w_scaled,
"x": x,
"w": w,
"matmul": matmul,
}
def _check_decoding(ctx: relax.transform.PatternCheckContext) -> bool:
call = ctx.annotated_expr["w"]
if not isinstance(call, relax.Call):
return False
g_var = call.args[0]
if not isinstance(g_var, relax.GlobalVar):
return False
return g_var.name_hint.startswith("dequantize") or g_var.name_hint.startswith(
"fused_dequantize"
)
def _check_matmul(ctx: relax.transform.PatternCheckContext) -> bool:
call = ctx.annotated_expr["matmul"]
if not isinstance(call, relax.Call):
return False
g_var = call.args[0]
if not isinstance(g_var, relax.GlobalVar):
return False
return (
g_var.name_hint.startswith("matmul")
or g_var.name_hint.startswith("fused_matmul")
or g_var.name_hint.startswith("NT_matmul")
or g_var.name_hint.startswith("fused_NT_matmul")
)
def _check(ctx: relax.transform.PatternCheckContext) -> bool:
return _check_decoding(ctx) and _check_matmul(ctx)
return matmul, annotations, _check
@@ -0,0 +1,91 @@
"""A compiler pass that fuses dequantize + take."""
import tvm
from tvm import IRModule, relax, tirx
from tvm.relax.dpl.pattern import (
GlobalVarPattern,
TuplePattern,
is_const,
is_op,
wildcard,
)
@tvm.transform.module_pass(opt_level=0, name="FuseDequantizeTake")
class FuseDequantizeTake:
"""A compiler pass that fuses dequantize + take."""
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
seq = []
for n_aux_tensor in [2, 3]:
for match_tir_vars in [False, True]:
seq.append(
relax.transform.FuseOpsByPattern(
[
(
"dequantize_take",
*_pattern(n_aux_tensor, match_tir_vars),
)
]
)
)
seq.append(relax.transform.FuseTIR())
mod = tvm.transform.Sequential(seq)(mod)
for g_var, func in mod.functions_items():
name = g_var.name_hint
if isinstance(func, tirx.PrimFunc) and (
("fused_dequantize" in name) and ("take" in name)
):
sch_mod = tvm.IRModule({"main": func})
sch_mod = tirx.transform.ForceNarrowIndexToInt32()(sch_mod)
sch = tvm.s_tir.Schedule(sch_mod)
sch.compute_inline("dequantize")
mod[g_var] = sch.mod["main"]
return mod
def _pattern(n_aux_tensor: int, match_tir_vars: bool):
dequantize = is_op("relax.call_tir")(
GlobalVarPattern(),
TuplePattern([wildcard() for _ in range(n_aux_tensor)]),
add_constraint=False,
)
indices = ~is_const()
if match_tir_vars:
call_tir_args_take = [
GlobalVarPattern(),
TuplePattern([dequantize, indices]),
wildcard(),
]
else:
call_tir_args_take = [
GlobalVarPattern(),
TuplePattern([dequantize, indices]),
]
take = is_op("relax.call_tir")(
*call_tir_args_take,
add_constraint=False,
)
annotations = {
"take": take,
"dequantize": dequantize,
"indices": indices,
}
def _check(ctx: relax.transform.PatternCheckContext) -> bool:
take = ctx.annotated_expr["take"]
dequantize = ctx.annotated_expr["dequantize"]
if not isinstance(dequantize, relax.Call):
return False
if not isinstance(take.args[0], relax.GlobalVar) or not isinstance(
dequantize.args[0], relax.GlobalVar
):
return False
return "take" in take.args[0].name_hint and "dequantize" in dequantize.args[0].name_hint
return take, annotations, _check
@@ -0,0 +1,107 @@
"""A compiler pass that fuses transpose + dequantize."""
import tvm
from tvm import relax, s_tir, tirx
from tvm.ir.module import IRModule
from tvm.relax.analysis import remove_all_unused
from tvm.relax.expr_functor import PyExprMutator, mutator
@tvm.transform.module_pass(opt_level=0, name="FuseDequantizeTranspose")
class FuseDequantizeTranspose:
"""A compiler pass that fuses transpose + dequantize."""
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
return _DequantizeTransposeFuser(mod).transform()
@mutator
class _DequantizeTransposeFuser(PyExprMutator):
def __init__(
self,
mod: IRModule,
):
super().__init__(mod)
self.mod = mod
def transform(self) -> IRModule:
"""Entry point"""
for g_var, func in self.mod.functions_items():
if isinstance(func, relax.Function):
updated_func = self.visit_expr(func)
updated_func = remove_all_unused(updated_func)
self.builder_.update_func(g_var, updated_func)
return self.builder_.get()
def visit_call_(
self,
call: relax.Call,
) -> relax.Expr:
call = self.visit_expr_post_order(call)
if call.op != tvm.ir.Op.get("relax.matmul"):
return call
# Do not fuse dequantize-transpose for GeMM
if (
call.args[0].ty.ndim < 2
or not isinstance(call.args[0].ty.shape[-2], tirx.IntImm)
or call.args[0].ty.shape[-2].value != 1
):
return call
matmul_rhs = self.lookup_binding(call.args[1])
if (
not isinstance(matmul_rhs, relax.Call)
or matmul_rhs.op != tvm.ir.Op.get("relax.permute_dims")
or matmul_rhs.args[0].ty.ndim != 2
or matmul_rhs.attrs.axes is not None
):
return call
transpose_input = self.lookup_binding(matmul_rhs.args[0])
if (
not isinstance(transpose_input, relax.Call)
or transpose_input.op != tvm.ir.Op.get("relax.call_tir")
or not transpose_input.args[0].name_hint.startswith("dequantize")
or not isinstance(transpose_input.ty, relax.TensorType)
):
return call
dequantize_tir_func = self.mod[transpose_input.args[0]]
assert isinstance(dequantize_tir_func, tirx.PrimFunc)
if (
len(dequantize_tir_func.body.block.alloc_buffers) != 1
or not isinstance(dequantize_tir_func.body.block.body, tirx.SeqStmt)
or len(dequantize_tir_func.body.block.body) != 2
or not isinstance(dequantize_tir_func.body.block.body[1], tirx.For)
or not isinstance(dequantize_tir_func.body.block.body[1].body.body, tirx.SBlockRealize)
or dequantize_tir_func.body.block.body[1].body.body.block.name_hint != "T_transpose"
):
return call
new_func_buffers = [
dequantize_tir_func.buffer_map[var] for var in dequantize_tir_func.params
]
new_func_buffers[-1] = dequantize_tir_func.body.block.alloc_buffers[0]
new_func = tirx.PrimFunc(
params=new_func_buffers,
body=tirx.SBlockRealize(
iter_values=[],
predicate=True,
block=tirx.SBlock(
iter_vars=[],
reads=[],
writes=[],
name_hint="root",
body=dequantize_tir_func.body.block.body[0],
),
),
)
# Call `renew_defs` for deep-copy to avoid IR node duplication in
# different PrimFuncs of an IRModule.
new_func = s_tir.renew_defs(new_func)
g_var = self.builder_.add_func(new_func, func_name="dequantize")
dequantize_matmul_rhs = self.builder_.emit(
relax.call_tir(g_var, transpose_input.args[1], out_ty=matmul_rhs.ty)
)
return relax.op.matmul(call.args[0], dequantize_matmul_rhs, out_dtype=call.attrs.out_dtype)
@@ -0,0 +1,331 @@
"""A compiler pass that fuses dequantize matmul + epilogue."""
import operator
from functools import reduce
import tvm
from tvm import IRModule, relax
from tvm.relax.dpl import rewrite_call
from tvm.relax.dpl.pattern import is_op, wildcard
@tvm.transform.module_pass(opt_level=0, name="FuseDequantizeEpilogue")
class FuseFTDequantizeEpilogue:
"""A compiler pass that fuses FasterTransformer dequantize matmul + epilogue."""
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
for gv, func in mod.functions_items():
if isinstance(func, relax.Function):
func = fuse_bias(func)
func = fuse_activation(func)
func = fuse_residual_binary(func)
func = fuse_residual_unary(func)
mod[gv] = func
return mod
def fuse_bias(func: relax.Function) -> relax.Function:
"""
Fuse following `relax.add` into fastertransformer.gemm_fp16_int as bias:
Before:
```
lv1 = relax.call_dps_packed("fastertransformer.gemm_fp16_int", ...)
lv2 = relax.add(lv1, bias)
```
After:
```
lv2 = relax.call_dps_packed("fastertransformer.gemm_fp16_int_bias", ..., bias, ...)
```
Parameters
----------
func : relax.Function
The function before fusion.
Returns
-------
ret : relax.Function
The function after fusion.
"""
decode_matmul = is_op("relax.call_dps_packed")(varg_default_wildcard=True)
bias = wildcard()
pattern = is_op("relax.add")(decode_matmul, bias) | is_op("relax.add")(bias, decode_matmul)
def rewriter(expr, match):
if match[decode_matmul].args[0].global_symbol == "fastertransformer.gemm_fp16_int":
assert len(match[decode_matmul].args) == 2
args_list = match[decode_matmul].args[1]
assert len(args_list) == 8
if not args_list[3].value == "identity":
# bias cannot be fused after activation
return expr
matched_bias = match[bias]
bias_stride = (
matched_bias.ty.shape[-1]
if bias
and not reduce(operator.mul, matched_bias.ty.shape, 1) == matched_bias.ty.shape[-1]
else 0
)
return relax.call_dps_packed(
"fastertransformer.gemm_fp16_int_bias",
[
args_list[0], # x
args_list[1], # weight
args_list[2], # scale
matched_bias, # bias
args_list[3], # activation
args_list[4], # m
args_list[5], # n
args_list[6], # k
args_list[7], # group_size
bias_stride, # bias_stride
],
out_ty=match[decode_matmul].ty,
)
return expr
return rewrite_call(pattern, rewriter, func)
def fuse_activation(func: relax.Function) -> relax.Function:
"""
Fuse following `relax.nn.silu/relu/gelu` into fastertransformer.gemm_fp16_int_bias
as activation:
Before:
```
lv1 = relax.call_dps_packed("fastertransformer.gemm_fp16_int_bias", ...)
lv2 = relax.silu(lv1)
```
After:
```
lv2 = relax.call_dps_packed("fastertransformer.gemm_fp16_int_bias", ..., "silu", ...)
```
Parameters
----------
func : relax.Function
The function before fusion.
Returns
-------
ret : relax.Function
The function after fusion.
"""
decode_matmul = is_op("relax.call_dps_packed")(varg_default_wildcard=True)
pattern = (
is_op("relax.nn.silu")(decode_matmul)
| is_op("relax.nn.gelu")(decode_matmul)
| is_op("relax.nn.relu")(decode_matmul)
)
def rewriter(expr, match):
if match[decode_matmul].args[0].global_symbol == "fastertransformer.gemm_fp16_int":
matched_activation = match[pattern]
assert matched_activation.op.name in [
"relax.nn.silu",
"relax.nn.gelu",
"relax.nn.relu",
]
assert len(match[decode_matmul].args) == 2
args_list = match[decode_matmul].args[1]
assert len(args_list) == 8
return relax.call_dps_packed(
"fastertransformer.gemm_fp16_int",
[
args_list[0], # x
args_list[1], # weight
args_list[2], # scale
matched_activation.op.name[9:], # activation
args_list[4], # m
args_list[5], # n
args_list[6], # k
args_list[7], # group_size
],
out_ty=match[decode_matmul].ty,
)
if match[decode_matmul].args[0].global_symbol == "fastertransformer.gemm_fp16_int_bias":
matched_activation = match[pattern]
assert matched_activation.op.name in [
"relax.nn.silu",
"relax.nn.gelu",
"relax.nn.relu",
]
assert len(match[decode_matmul].args) == 2
args_list = match[decode_matmul].args[1]
assert len(args_list) == 10
return relax.call_dps_packed(
"fastertransformer.gemm_fp16_int_bias",
[
args_list[0], # x
args_list[1], # weight
args_list[2], # scale
args_list[3], # bias
matched_activation.op.name[9:], # activation
args_list[5], # m
args_list[6], # n
args_list[7], # k
args_list[8], # group_size
args_list[9], # bias_stride
],
out_ty=match[decode_matmul].ty,
)
return expr
return rewrite_call(pattern, rewriter, func)
def fuse_residual_binary(func: relax.Function) -> relax.Function:
"""
Fuse following `relax.add/multiply` into fastertransformer.gemm_fp16_int_bias as
residual binary operation:
Before:
```
lv1 = relax.call_dps_packed("fastertransformer.gemm_fp16_int_bias", ...)
lv2 = relax.add(lv1, residual)
```
After:
```
lv2 = relax.call_dps_packed(
"fastertransformer.gemm_fp16_int_bias_residual",
...,
residual,
...,
"plus",
...
)
```
Parameters
----------
func : relax.Function
The function before fusion.
Returns
-------
ret : relax.Function
The function after fusion.
"""
decode_matmul = is_op("relax.call_dps_packed")(varg_default_wildcard=True)
residual = wildcard()
pattern = (
is_op("relax.add")(decode_matmul, residual)
| is_op("relax.add")(residual, decode_matmul)
| is_op("relax.multiply")(decode_matmul, residual)
| is_op("relax.multiply")(residual, decode_matmul)
)
def rewriter(expr, match):
if match[decode_matmul].args[0].global_symbol == "fastertransformer.gemm_fp16_int_bias":
matched_binary = match[pattern]
assert matched_binary.op.name in ["relax.add", "relax.multiply"]
binary_op = "plus" if matched_binary.op.name == "relax.add" else "multiply"
assert len(match[decode_matmul].args) == 2
args_list = match[decode_matmul].args[1]
assert len(args_list) == 10
matched_residual = match[residual]
if not args_list[9].value == 0:
# fastertransformer.gemm_fp16_int_bias_residual does not support
# bias_stride != 0 yet
return expr
return relax.call_dps_packed(
"fastertransformer.gemm_fp16_int_bias_residual",
[
args_list[0], # x
args_list[1], # weight
args_list[2], # scale
args_list[3], # bias
matched_residual, # residual
args_list[4], # activation
binary_op, # binary_op
"identity", # unary_op
args_list[5], # m
args_list[6], # n
args_list[7], # k
args_list[8], # group_size
],
out_ty=match[decode_matmul].ty,
)
return expr
return rewrite_call(pattern, rewriter, func)
def fuse_residual_unary(func: relax.Function) -> relax.Function:
"""
Fuse following `relax.nn.silu/relu/gelu` into fastertransformer.gemm_fp16_int_bias_residual
as residual unary operation:
Before:
```
lv1 = relax.call_dps_packed("fastertransformer.gemm_fp16_int_bias_residual", ...)
lv2 = relax.silu(lv1)
```
After:
```
lv2 = relax.call_dps_packed("fastertransformer.gemm_fp16_int_bias_residual", ..., "silu", ...)
```
Parameters
----------
func : relax.Function
The function before fusion.
Returns
-------
ret : relax.Function
The function after fusion.
"""
decode_matmul = is_op("relax.call_dps_packed")(varg_default_wildcard=True)
pattern = (
is_op("relax.nn.silu")(decode_matmul)
| is_op("relax.nn.gelu")(decode_matmul)
| is_op("relax.nn.relu")(decode_matmul)
)
def rewriter(expr, match):
if (
match[decode_matmul].args[0].global_symbol
== "fastertransformer.gemm_fp16_int_bias_residual"
):
matched_activation = match[pattern]
assert matched_activation.op.name in [
"relax.nn.silu",
"relax.nn.gelu",
"relax.nn.relu",
]
assert len(match[decode_matmul].args) == 2
args_list = match[decode_matmul].args[1]
assert len(args_list) == 12
return relax.call_dps_packed(
"fastertransformer.gemm_fp16_int_bias_residual",
[
args_list[0], # x
args_list[1], # weight
args_list[2], # scale
args_list[3], # bias
args_list[4], # residual
args_list[5], # activation
args_list[6], # binary_op
matched_activation.op.name[9:], # activation
args_list[8], # m
args_list[9], # n
args_list[10], # k
args_list[11], # group_size
],
out_ty=match[decode_matmul].ty,
)
return expr
return rewrite_call(pattern, rewriter, func)
@@ -0,0 +1,145 @@
"""A compiler pass that fuses transpose + matmul."""
import tvm
from tvm import IRModule, relax, te, tirx
from tvm.relax.dpl.pattern import is_op, wildcard
from tvm.relax.expr_functor import PyExprMutator, mutator
@tvm.transform.module_pass(opt_level=0, name="FuseTransposeMatmul")
class FuseTransposeMatmul:
"""A compiler pass that fuses transpose + matmul."""
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
mod = relax.transform.FuseOpsByPattern(
[
(
"transpose_matmul_fuse",
*_pattern(),
),
]
)(mod)
transpose_matmul_codegen = _TransposeMatmulFuser(mod)
for g_var, func in mod.functions_items():
if isinstance(func, relax.Function):
func = transpose_matmul_codegen.visit_expr(func)
transpose_matmul_codegen.builder_.update_func(g_var, func)
return transpose_matmul_codegen.builder_.get()
def _pattern():
"""Pattern for transpose + matmul."""
w = wildcard()
x = wildcard()
wT = is_op("relax.permute_dims")(w)
o = is_op("relax.matmul")(x, wT)
annotations = {"o": o, "w": w, "x": x, "wT": wT}
def _check(context: relax.transform.PatternCheckContext) -> bool:
transpose_call = context.annotated_expr["wT"]
ndim = transpose_call.args[0].ty.ndim
if ndim == -1:
return False
if ndim == 2 and transpose_call.attrs.axes is None:
return True
axes = list(range(ndim))
axes[-1], axes[-2] = axes[-2], axes[-1]
return list(transpose_call.attrs.axes) == axes
return o, annotations, _check
@mutator
class _TransposeMatmulFuser(PyExprMutator):
def __init__(self, mod):
super().__init__(mod)
def visit_call_(
self,
call: relax.Call,
) -> relax.Expr:
out_dtype = None
def te_transposed_matmul(a: te.Tensor, b: te.Tensor) -> te.Tensor:
nonlocal out_dtype
a_shape = list(a.shape)
b_shape = list(b.shape)
a_prepended = False
b_appended = False
if len(a_shape) == 1:
a_prepended = True
a_shape.insert(0, 1)
if len(b_shape) == 1:
b_appended = True
b_shape.append(1)
is_a_larger = len(a_shape) > len(b_shape)
offset = len(a_shape) - len(b_shape) if is_a_larger else len(b_shape) - len(a_shape)
a_relax = relax.Var("a", relax.TensorType(a.shape))
bT_shape = list(b.shape)
bT_shape[-1], bT_shape[-2] = bT_shape[-2], bT_shape[-1]
bT_relax = relax.Var("b", relax.TensorType(bT_shape))
output_shape = self.builder_.normalize(relax.op.matmul(a_relax, bT_relax)).ty.shape
def matmul_compute(*idx_spatial):
k = te.reduce_axis((0, a_shape[-1]), name="k")
def multiply_compute(idx_reduce):
a_indices = []
b_indices = []
for i in range(offset):
if is_a_larger:
a_indices.append(idx_spatial[i])
else:
b_indices.append(idx_spatial[i])
for i in range(offset, len(output_shape) - (2 - a_prepended - b_appended)):
a_dim = a_shape[i if is_a_larger else i - offset]
b_dim = b_shape[i if not is_a_larger else i - offset]
dim_equal = a_dim == b_dim
if not isinstance(dim_equal, tirx.IntImm) or dim_equal == 0:
a_dim_is_one = isinstance(a_dim, tirx.IntImm) and a_dim == 1
b_dim_is_one = isinstance(b_dim, tirx.IntImm) and b_dim == 1
a_indices.append(0 if a_dim_is_one else idx_spatial[i])
b_indices.append(0 if b_dim_is_one else idx_spatial[i])
else:
a_indices.append(idx_spatial[i])
b_indices.append(idx_spatial[i])
if not a_prepended:
a_indices.append(idx_spatial[-2 + b_appended])
a_indices.append(idx_reduce)
if not b_appended:
b_indices.append(idx_spatial[-1])
b_indices.append(idx_reduce)
dtype = out_dtype
if dtype != "":
return a(*a_indices).astype(dtype) * b(*b_indices).astype(dtype)
return a(*a_indices) * b(*b_indices)
return te.sum(multiply_compute(k), axis=k)
return te.compute(
output_shape,
lambda *idx: matmul_compute(*idx),
name="NT_matmul",
)
if isinstance(call.op, relax.GlobalVar):
function = self.builder_.get()[call.op]
if (
"Composite" in function.attrs
and function.attrs["Composite"] == "transpose_matmul_fuse"
):
out_dtype = function.ret_ty.dtype
return self.builder_.call_te(
te_transposed_matmul,
call.args[1],
call.args[0],
primfunc_name_hint="NT_matmul",
)
return super().visit_call_(call)
@@ -0,0 +1,198 @@
"""A compiler pass that lifts TIR-level global allocation to Relax."""
from typing import Dict, List, Tuple # noqa: UP035
import tvm
from tvm import relax, tirx
from tvm.ir.module import IRModule
from tvm.relax.analysis import remove_all_unused
from tvm.relax.expr_functor import PyExprMutator, mutator
@tvm.transform.module_pass(opt_level=0, name="LiftTIRGlobalBufferAlloc")
class LiftTIRGlobalBufferAlloc:
"""A compiler pass that lifts TIR-level global allocation to Relax."""
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
return _TIRGlobalAllocRewriter(mod).transform()
@mutator
class _TIRGlobalAllocRewriter(PyExprMutator):
def __init__(self, mod: IRModule):
super().__init__(mod)
self.mod = mod
self.gv2new_tensor_sinfo: Dict[ # noqa: UP006
tvm.ir.GlobalVar,
Tuple[tvm.ir.GlobalVar, List[relax.TensorType], tirx.PrimFunc], # noqa: UP006
] = {}
def transform(self) -> IRModule:
"""Entry point of the transformation"""
for g_var, func in self.mod.functions_items():
if isinstance(func, tirx.PrimFunc):
updated_func, tensor_sinfo_list = remove_global_buf_alloc(func)
if len(tensor_sinfo_list) > 0:
new_gv = self.builder_.add_func(updated_func, g_var.name_hint)
self.gv2new_tensor_sinfo[g_var] = (new_gv, tensor_sinfo_list, func)
self.mod = self.builder_.get()
for g_var, func in self.mod.functions_items():
if isinstance(func, relax.Function):
updated_func = self.visit_expr(func)
updated_func = remove_all_unused(updated_func)
self.builder_.update_func(g_var, updated_func)
mod = self.builder_.get()
return relax.transform.DeadCodeElimination()(mod)
def visit_call_(self, call: relax.Call):
call = self.visit_expr_post_order(call)
if (
call.op != tvm.ir.Op.get("relax.call_tir")
or call.args[0] not in self.gv2new_tensor_sinfo
):
return call
g_var = call.args[0]
new_gv, tensor_sinfo, func_before_update = self.gv2new_tensor_sinfo[g_var]
assert len(call.ty_args) == 1
if any(_has_symbolic_var(sinfo) for sinfo in tensor_sinfo):
tensor_sinfo, success = _resolve_tir_var_mapping(func_before_update, call, tensor_sinfo)
if not success:
# Cannot resolve TIR var mapping. Fall back to no lifting.
self.gv2new_tensor_sinfo.pop(g_var)
return call
args = list(call.args)
args[0] = new_gv
if isinstance(call.ty_args[0], relax.TensorType):
new_call = relax.Call(
call.op,
args=args,
ty_args=[relax.TupleType(list(call.ty_args) + tensor_sinfo)],
attrs=call.attrs,
)
emitted_tuple = self.builder_.emit(new_call)
return relax.TupleGetItem(emitted_tuple, 0)
assert isinstance(call.ty_args[0], relax.TupleType)
return relax.Call(
call.op,
args=args,
ty_args=[relax.TupleType(list(call.ty_args[0].fields) + tensor_sinfo)],
attrs=call.attrs,
)
def remove_global_buf_alloc(
func: tirx.PrimFunc,
) -> Tuple[tirx.PrimFunc, List[relax.TensorType]]: # noqa: UP006
"""Remove the global buffer allocation for a given TIR PrimFunc."""
assert isinstance(func.body, tirx.SBlockRealize)
params = list(func.params)
buffer_map = dict(func.buffer_map)
tensor_sinfo = []
alloc_buffers = []
insertion_point = len(params)
while not isinstance(params[insertion_point - 1].ty, tvm.ir.PointerType):
insertion_point -= 1
assert insertion_point >= 1
prev_root_block = func.body.block
for buf_alloc in func.body.block.alloc_buffers:
if buf_alloc.scope() == "global":
param = tirx.Var("var_" + buf_alloc.name, "handle")
params.insert(insertion_point, param)
insertion_point += 1
buffer_map[param] = buf_alloc
tensor_sinfo.append(relax.TensorType(buf_alloc.shape, buf_alloc.dtype))
else:
alloc_buffers.append(buf_alloc)
if len(tensor_sinfo) == 0:
return func, []
assert len(prev_root_block.iter_vars) == 0
assert len(prev_root_block.reads) == 0
assert len(prev_root_block.writes) == 0
assert len(prev_root_block.match_buffers) == 0
assert prev_root_block.name_hint == "root"
assert prev_root_block.init is None
root_block = tirx.SBlock(
iter_vars=[],
reads=[],
writes=[],
name_hint="root",
body=prev_root_block.body,
alloc_buffers=alloc_buffers,
annotations=prev_root_block.annotations,
)
updated_func = tirx.PrimFunc(
params=params,
body=tirx.SBlockRealize(iter_values=[], predicate=True, block=root_block),
ret_type=func.ret_type,
buffer_map=buffer_map,
attrs=func.attrs,
)
return updated_func, tensor_sinfo
def _has_symbolic_var(tensor_sinfo: relax.TensorType) -> bool:
assert isinstance(tensor_sinfo.shape, relax.ShapeExpr)
for dim in tensor_sinfo.shape.values:
if not isinstance(dim, tirx.IntImm):
return True
return False
def _resolve_tir_var_mapping(
func: tirx.PrimFunc,
call: relax.Call,
tensor_sinfo: List[relax.TensorType], # noqa: UP006
) -> Tuple[List[relax.TensorType], bool]: # noqa: UP006
"""Resolve the TIR symbolic var relationship across sides of PrimFunc and Relax Function"""
var_map: Dict[tirx.Var, tirx.Expr] = {} # noqa: UP006
n_arg = len(call.args[1].fields)
for i in range(n_arg):
buffer_shape = func.buffer_map[func.params[i]].shape
arg_shape = call.args[1][i].ty.shape.values
assert len(buffer_shape) == len(arg_shape)
for v_l, v_r in zip(buffer_shape, arg_shape):
if isinstance(v_l, tirx.Var):
var_map[v_l] = v_r
elif not isinstance(v_l, tirx.IntImm):
return [], False
ret_tensors = call.ty_args[0]
ret_tensors = (
[ret_tensors] if isinstance(ret_tensors, relax.TensorType) else list(ret_tensors.fields)
)
for i, ret_tensor in enumerate(ret_tensors):
buffer_shape = func.buffer_map[func.params[n_arg + i]].shape
ret_tensor_shape = ret_tensor.shape.values
assert len(buffer_shape) == len(ret_tensor_shape)
for v_l, v_r in zip(buffer_shape, ret_tensor_shape):
if isinstance(v_l, tirx.Var):
var_map[v_l] = v_r
elif not isinstance(v_l, tirx.IntImm):
return [], False
updated_tensor_sinfo = []
for sinfo in tensor_sinfo:
if not _has_symbolic_var(sinfo):
updated_tensor_sinfo.append(sinfo)
continue
new_shape = []
for dim in sinfo.shape.values:
new_shape.append(tirx.stmt_functor.substitute(dim, var_map))
updated_tensor_sinfo.append(relax.TensorType(new_shape, sinfo.dtype))
return updated_tensor_sinfo, True
@@ -0,0 +1,64 @@
"""A compiler pass that dispatch low-batch-gemm to gemv schedule."""
import tvm
import tvm_ffi
from tvm import tirx
from tvm.ir.module import IRModule
from tvm.s_tir import dlight as dl
@tvm.transform.module_pass(opt_level=0, name="LowBatchGemvSpecialize")
class LowBatchGemvSpecialize:
"""A compiler pass that dispatch low-batch-gemm to gemv schedule."""
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
for g_var, func in mod.functions_items():
if isinstance(func, tirx.PrimFunc):
low_batch_range = [2, 8]
buckets = [2, 4]
low_batch_funcs = []
for bucket in buckets:
low_batch_mod = IRModule({})
low_batch_mod["main"] = func
low_batch_mod = dl.ApplyDefaultSchedule(
dl.gpu.LowBatchGEMV(bucket),
)(low_batch_mod)
low_batch_funcs.append(low_batch_mod["main"])
if any(
tvm_ffi.structural_equal(low_batch_func, func)
for low_batch_func in low_batch_funcs
):
continue
buffers = func.buffer_map.values()
shapes = [buffer.shape for buffer in buffers]
symbolic_vars = set(
expr for shape in shapes for expr in shape if isinstance(expr, tirx.Var)
)
if len(symbolic_vars) != 1:
continue
gemm_mod = IRModule({})
gemm_mod["main"] = func
gemm_mod = dl.ApplyDefaultSchedule(
dl.gpu.Matmul(),
)(gemm_mod)
gemm_func = gemm_mod["main"]
sym_var = next(iter(symbolic_vars))
body = gemm_func.body
for i, range_limit in reversed(list(enumerate(low_batch_range))):
body = tirx.IfThenElse(
tirx.op.tvm_thread_invariant(sym_var <= range_limit),
low_batch_funcs[i].body,
body,
)
body = tirx.SBlock([], [], [], "root", body)
body = tirx.SBlockRealize([], True, body)
new_func = func.with_body(body)
new_func = new_func.with_attr("tirx.is_scheduled", 1)
new_func = new_func.with_attr("tirx.HoistIfThenElseExprWithBlock", 1)
mod.update_func(g_var, new_func)
return mod
+209
View File
@@ -0,0 +1,209 @@
"""The compilation pipeline for LLM applications."""
from pathlib import Path
from typing import Any, Dict, List, Optional # noqa: UP035
import tvm
from tvm import IRModule
from tvm.relax import register_pipeline
from tvm.relax.frontend import nn
from tvm.s_tir import dlight as dl
from mlc_llm.interface.compiler_flags import IPCAllReduceStrategyType
from mlc_llm.support import logging
from .attach_cuda_graph_alloc_init_func import AttachCUDAGraphAllocInitFunc
from .attach_embedding_allocator import AttachAllocEmbeddingTensorFunc
from .attach_logit_processor import AttachLogitProcessFunc
from .attach_sampler import AttachGPUSamplingFunc
from .attach_softmax_with_temperature import AttachSoftmaxWithTemperature
from .attach_spec_decode_aux_funcs import AttachSpecDecodeAuxFuncs
from .attach_support_info import (
AttachAdditionalPrimFuncs,
AttachCUDAGraphSymbolicCaptureHints,
AttachMemoryPlanAttr,
AttachPipelineParallelStages,
AttachSequenceLengthPaddingFactor,
AttachVariableBounds,
)
from .blas_dispatch import BLASDispatch
from .clean_up_tir_attrs import CleanUpTIRAttrs
from .dispatch_kv_cache_creation import DispatchKVCacheCreation
from .dispatch_triton_kernel import DispatchTritonKernel
from .estimate_memory_usage import AttachMetadataWithMemoryUsage
from .fuse_add_norm import FuseAddRMSNorm
from .fuse_dequantize_matmul_ewise import FuseDequantizeMatmulEwise
from .fuse_dequantize_take import FuseDequantizeTake
from .fuse_dequantize_transpose import FuseDequantizeTranspose
from .fuse_ft_dequantize_matmul_epilogue import FuseFTDequantizeEpilogue
from .fuse_transpose_matmul import FuseTransposeMatmul
from .lift_global_buffer_alloc import LiftTIRGlobalBufferAlloc
from .low_batch_specialization import LowBatchGemvSpecialize
from .pipeline_parallel_rewrite import PipelineParallelRewrite
from .scatter_tuple_get_item import ScatterTupleGetItem
logger = logging.getLogger(__name__)
@tvm.transform.module_pass(opt_level=0, name="_LogProgress")
class _LogProgress:
"""A dummy compiler pass that does nothing but logging."""
def __init__(self, *args):
self.args = args
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""A dummy transformation"""
logger.info(*self.args)
return mod
@tvm.transform.module_pass(opt_level=0, name="DebugDump")
class _DebugDump:
"""A dummy compiler pass that does nothing but logging.
Only enabled when debug_dump is not None"""
def __init__(self, file_name: str, file_path: Optional[Path], show_meta: bool = False):
self.file_name = file_name
self.file_path = file_path
self.show_meta = show_meta
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""A dummy transformation that dumps the module to file"""
if self.file_path is not None:
# NOTE: We use debug level here to avoid spamming the console
logger.debug("Dumping IR to %s", self.file_path / self.file_name)
with open(self.file_path / self.file_name, "w", encoding="utf-8") as f:
f.write(mod.script(show_meta=self.show_meta))
return mod
@register_pipeline("mlc_llm")
def _mlc_llm_pipeline(
target: tvm.target.Target,
flashinfer: bool = False,
cublas_gemm: bool = False,
faster_transformer: bool = False,
allreduce_strategy: IPCAllReduceStrategyType = IPCAllReduceStrategyType.NONE,
variable_bounds: Optional[Dict[str, int]] = None, # noqa: UP006
cuda_graph_symbolic_capture_hints: Optional[Dict[str, List[str]]] = None, # noqa: UP006
additional_tirs: Optional[Dict[str, tvm.tirx.PrimFunc]] = None, # noqa: UP006
metadata: Optional[Dict[str, Any]] = None, # noqa: UP006
ext_mods: Optional[List[nn.ExternModule]] = None, # noqa: UP006
debug_dump: Optional[Path] = None,
):
variable_bounds = variable_bounds or {}
cuda_graph_symbolic_capture_hints = cuda_graph_symbolic_capture_hints or {}
additional_tirs = additional_tirs or {}
metadata = metadata or {}
ext_mods = ext_mods or []
tensor_parallel_shards = metadata.get("tensor_parallel_shards", 1)
@tvm.transform.module_pass(opt_level=0)
def _pipeline(mod: tvm.ir.IRModule, _ctx: tvm.transform.PassContext) -> tvm.ir.IRModule:
seq = tvm.transform.Sequential(
[
# Phase 0. Add additional information for compilation and remove unused Relax func
DispatchKVCacheCreation(target, flashinfer, metadata),
AttachSoftmaxWithTemperature(target, metadata),
AttachVariableBounds(variable_bounds),
AttachCUDAGraphSymbolicCaptureHints(cuda_graph_symbolic_capture_hints),
AttachPipelineParallelStages(metadata["pipeline_parallel_stages"]),
AttachLogitProcessFunc(target),
AttachAdditionalPrimFuncs(additional_tirs),
AttachAllocEmbeddingTensorFunc(metadata),
AttachGPUSamplingFunc(target, variable_bounds),
AttachSpecDecodeAuxFuncs(tensor_parallel_shards),
AttachMemoryPlanAttr(),
AttachSequenceLengthPaddingFactor(target, metadata),
tvm.tirx.transform.BindTarget(tvm.target.Target.current(allow_none=False)),
_DebugDump("debug-phase0.py", debug_dump, show_meta=False),
# Phase 1. Passes on high-level operator graph
_LogProgress("Running TVM Relax graph-level optimizations"),
DispatchTritonKernel(target),
FuseFTDequantizeEpilogue(),
FuseDequantizeTranspose(),
BLASDispatch(target) if cublas_gemm else tvm.transform.Sequential([]),
(
FuseAddRMSNorm(target=target)
if target.kind.name != "llvm"
else tvm.transform.Sequential([])
),
FuseTransposeMatmul(),
_DebugDump("debug-phase1.py", debug_dump, show_meta=False),
# Phase 2. Lowering to TIR, inherited TVM Relax's official "zero" pipeline
_LogProgress("Lowering to TVM TIR kernels"),
tvm.relax.backend.DispatchSampling(),
tvm.relax.backend.DispatchSortScan(),
tvm.relax.transform.LegalizeOps(),
tvm.relax.transform.AnnotateTIROpPattern(),
tvm.relax.transform.FoldConstant(),
tvm.relax.transform.FuseOps(),
tvm.relax.transform.FuseTIR(),
_DebugDump("debug-phase2.py", debug_dump, show_meta=False),
# Phase 3. Passes on TIR
_LogProgress("Running TVM TIR-level optimizations"),
FuseDequantizeMatmulEwise(),
FuseDequantizeTake(),
tvm.relax.transform.DeadCodeElimination(),
CleanUpTIRAttrs(["op_pattern"]),
_DebugDump("debug-phase3.py", debug_dump, show_meta=False),
# Phase 4. Low-level Optimizations
_LogProgress("Running TVM Dlight low-level optimizations"),
LowBatchGemvSpecialize(),
(
dl.ApplyDefaultSchedule(
dl.gpu.Matmul(),
dl.gpu.GEMV(),
dl.gpu.Reduction(),
dl.gpu.GeneralReduction(),
dl.gpu.Fallback(),
)
if target.kind.name != "llvm"
else dl.ApplyDefaultSchedule(
dl.cpu.GEMV(),
)
),
_DebugDump("debug-phase4.py", debug_dump, show_meta=False),
_LogProgress("Lowering to VM bytecode"),
(
LiftTIRGlobalBufferAlloc()
if target.kind.name != "llvm"
else tvm.transform.Sequential([])
),
(
tvm.tirx.transform.ForceNarrowIndexToInt32()
if target.kind.name != "cuda"
else tvm.transform.Sequential([])
),
ScatterTupleGetItem(),
PipelineParallelRewrite(),
tvm.relax.transform.RewriteDataflowReshape(),
tvm.relax.transform.ToNonDataflow(),
tvm.relax.transform.RemovePurityChecking(),
tvm.relax.transform.CallTIRRewrite(),
(
tvm.relax.transform.IPCAllReduceRewrite(allreduce_strategy)
if allreduce_strategy != IPCAllReduceStrategyType.NONE
else tvm.transform.Sequential([])
),
tvm.relax.transform.StaticPlanBlockMemory(),
AttachMetadataWithMemoryUsage(metadata),
_DebugDump("debug-phase5.py", debug_dump, show_meta=False),
tvm.relax.transform.RewriteCUDAGraph(),
AttachCUDAGraphAllocInitFunc(),
tvm.relax.transform.LowerGPUIPCAllocStorage(),
tvm.relax.transform.LowerAllocTensor(),
tvm.relax.transform.KillAfterLastUse(),
tvm.relax.transform.LowerRuntimeBuiltin(),
tvm.relax.transform.VMShapeLower(),
tvm.relax.transform.AttachGlobalSymbol(),
_LogProgress("Compiling external modules"),
tvm.relax.transform.AttachExternModules(ext_mods),
_LogProgress("Compilation complete! Exporting to disk"),
]
)
mod = seq(mod)
return mod
return _pipeline
@@ -0,0 +1,399 @@
"""A compiler pass that rewrites IR for pipeline parallelism."""
from typing import Dict, List, Optional, Tuple # noqa: UP035
import tvm
from tvm import relax, tirx
from tvm.ir.module import IRModule
from tvm.relax.expr_functor import PyExprMutator, PyExprVisitor, mutator, visitor
@tvm.transform.module_pass(opt_level=0, name="PipelineParallelRewrite")
class PipelineParallelRewrite:
"""A compiler pass that rewrites IR for pipeline parallelism."""
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
return _PipelineParallelRewriter(mod.clone()).transform()
@mutator
class _PipelineParallelRewriter(PyExprMutator):
def __init__(self, mod: IRModule):
super().__init__(mod)
self.mod = mod
self.old_packed_params_var: relax.Var
self.new_main_packed_params_var: relax.Var
self.new_stage_func_packed_params: relax.Var
self.undefined_shape_vars_remap: Dict[tirx.Var, tirx.Var] # noqa: UP006
self.undefined_param_shape_vars_remap: Dict[tirx.Var, tirx.Var] # noqa: UP006
def transform(self) -> IRModule:
"""Entry point of the transformation"""
for g_var, func in self.mod.functions_items():
if not isinstance(func, relax.Function) or "pipeline_parallel_stages" not in func.attrs:
continue
num_stages = int(func.attrs["pipeline_parallel_stages"])
if num_stages == 1:
continue
pipeline_stages, stage_send_vars, stage_receive_vars = _extract_pipeline_stages(func)
assert len(pipeline_stages) == num_stages, (
"Number of pipeline stages mismatches: "
f"expecting {num_stages} stages, but {len(pipeline_stages)} are found in the IR."
)
required_func_params = _analyze_required_func_params(pipeline_stages, func.params)
assert "num_input" in func.attrs
num_input = int(func.attrs["num_input"])
assert (
len(func.params) == num_input + 1
and isinstance(func.params[num_input], relax.Var)
and func.params[num_input].name_hint == "packed_params"
), 'Only the extra "packed_params" parameter is allowed'
self.old_packed_params_var = func.params[num_input]
self.new_main_packed_params_var = relax.Var("packed_params", relax.ObjectType())
for required_params in required_func_params:
for i, param in enumerate(required_params):
if param.same_as(self.old_packed_params_var):
required_params.pop(i)
break
func_output = func.body.body
assert isinstance(func_output, relax.Var)
stage_func_gvs = []
caller_args_list = []
for i in range(num_stages):
stage_func_gv, caller_args = self._create_stage_func(
g_var.name_hint + f"_stage{i}",
pipeline_stages[i],
required_func_params[i],
stage_receive_vars[i],
stage_send_vars[i],
func.attrs,
func_output=func_output if i == num_stages - 1 else None,
)
stage_func_gvs.append(stage_func_gv)
caller_args_list.append(caller_args)
# Create and update the entry function, which dispatches toz the stage functions
# according to the disco worker group id.
bb = relax.BlockBuilder()
params = [*list(func.params[:-1]), self.new_main_packed_params_var]
with bb.function(g_var.name_hint, params=params):
dispatch_func_args = []
for stage_func_gv, caller_args in zip(stage_func_gvs, caller_args_list):
dispatch_func_args.append([stage_func_gv, *caller_args])
output = bb.emit(
relax.op.call_builtin_with_ctx(
"mlc.multi_gpu.DispatchFunctionByGroup",
args=[dispatch_func_args],
ty_args=relax.ObjectType(),
)
)
dispatch_func_gv = bb.emit_func_output(output)
dispatch_func = bb.finalize()[dispatch_func_gv]
self.builder_.update_func(g_var, dispatch_func)
return self.builder_.finalize()
def _create_stage_func(
self,
func_name: str,
stage_bindings: List[relax.Binding], # noqa: UP006
required_func_params: List[relax.Var], # noqa: UP006
stage_receive_vars: List[relax.Var], # noqa: UP006
stage_send_vars: List[relax.Var], # noqa: UP006
func_attrs: tvm.ir.DictAttrs,
func_output: Optional[relax.Var],
) -> Tuple[tvm.ir.GlobalVar, List[relax.Expr]]: # noqa: UP006
self.undefined_shape_vars_remap = {}
self.undefined_param_shape_vars_remap = {}
# Prepare the func parameters (except the shape variables and packed params)
params, args = self._prepare_stage_func_params_and_args(required_func_params)
for new_param, old_param in zip(params, required_func_params):
self.set_var_remap(old_param, new_param)
# Create new packed params
self.new_stage_func_packed_params = relax.Var("packed_params", relax.ObjectType())
self.set_var_remap(self.old_packed_params_var, self.new_stage_func_packed_params)
new_func_outputs = []
with self.builder_.function(func_name, pure=False):
with self.builder_.dataflow():
# Emit the tensors received from last stage.
for receive_var in stage_receive_vars:
new_receive_var = self.builder_.emit(
relax.call_dps_packed(
"runtime.disco.recv_from_prev_group",
args=[],
out_ty=self._update_struct_info(receive_var.ty),
),
name_hint=receive_var.name_hint,
)
self.set_var_remap(receive_var, new_receive_var)
# Process the bindings in this stage.
for stage_binding in stage_bindings:
if stage_binding.var in stage_send_vars or stage_binding.var.same_as(
func_output
):
assert isinstance(stage_binding, relax.VarBinding)
new_var = self.builder_.emit_output(
self.visit_expr(stage_binding.value),
name_hint=stage_binding.var.name_hint,
)
self.set_var_remap(stage_binding.var, new_var)
new_func_outputs.append(new_var)
else:
self.visit_binding(stage_binding)
# Emit the calls to send tensors to the next stage.
for send_var in stage_send_vars:
new_send_var = self.get_var_remap(send_var)
self.builder_.emit(
relax.Call(
relax.ExternFunc("runtime.disco.send_to_next_group"),
args=[new_send_var],
ty_args=None,
)
)
# Create the param for the shape variables.
shape_var_params = []
shape_var_args = []
for (
shape_var_arg,
shape_var_param,
) in self.undefined_shape_vars_remap.items():
if shape_var_arg not in self.undefined_param_shape_vars_remap:
shape_var_params.append(shape_var_param)
shape_var_args.append(shape_var_arg)
params.append(relax.Var("s", relax.ShapeType(shape_var_params)))
args.append(relax.ShapeExpr(shape_var_args))
# Add the packed params.
params.append(self.new_stage_func_packed_params)
args.append(self.new_main_packed_params_var)
# Conclude the function.
if func_output is not None:
assert len(new_func_outputs) == 1
new_gv = self.builder_.emit_func_output(
(
new_func_outputs[0]
if len(new_func_outputs) == 1
and isinstance(new_func_outputs[0].ty, relax.TupleType)
else new_func_outputs
),
params=params,
)
new_func = (
self.builder_.get()[new_gv]
.with_attrs(func_attrs)
.with_attr("num_input", len(params) - 1)
.without_attr("global_symbol")
.without_attr("pipeline_parallel_stages")
)
self.builder_.update_func(new_gv, new_func)
return new_gv, args
def visit_var_binding_(self, binding: relax.VarBinding) -> None:
if not isinstance(binding.value, relax.TupleGetItem):
super().visit_var_binding_(binding)
return
tuple_value = self.visit_expr(binding.value.tuple_value)
if not tuple_value.same_as(self.new_stage_func_packed_params):
super().visit_var_binding_(binding)
return
assert isinstance(binding.var.ty, relax.TensorType)
cur_num_undefined_param_shape_vars = len(self.undefined_param_shape_vars_remap)
new_tensor_struct_info = self._update_struct_info(
binding.var.ty, self.undefined_param_shape_vars_remap
)
has_new_undefined_shape_var = (
len(self.undefined_param_shape_vars_remap) != cur_num_undefined_param_shape_vars
)
self.undefined_shape_vars_remap = {
**self.undefined_shape_vars_remap,
**self.undefined_param_shape_vars_remap,
}
ret_sinfo = (
new_tensor_struct_info if not has_new_undefined_shape_var else relax.ObjectType()
)
call = relax.call_pure_packed(
"vm.builtin.tuple_getitem",
self.new_stage_func_packed_params,
relax.prim_value(binding.value.index),
ty_args=ret_sinfo,
)
new_binding_var = self.builder_.emit(call, binding.var.name_hint)
if has_new_undefined_shape_var:
new_binding_var = self.builder_.match_cast(
new_binding_var, new_tensor_struct_info, binding.var.name_hint + "_cast"
)
self.set_var_remap(binding.var, new_binding_var)
def visit_call_(self, call: relax.Call) -> relax.Call:
call = super().visit_call_(call)
return relax.Call(
call.op,
call.args,
call.attrs,
ty_args=[self._update_struct_info(struct_info) for struct_info in call.ty_args],
)
def _prepare_stage_func_params_and_args(
self,
required_func_params: List[relax.Var], # noqa: UP006
) -> Tuple[List[relax.Var], List[relax.Expr]]: # noqa: UP006
params: List[relax.Var] = [] # noqa: UP006
args: List[relax.Expr] = [] # noqa: UP006
for required_param in required_func_params:
struct_info = self._update_struct_info(required_param.ty)
params.append(relax.Var(required_param.name_hint, struct_info))
args.append(required_param)
return params, args
def _update_struct_info(
self,
struct_info: relax.Type,
undefined_var_remap: Optional[Dict[tirx.Var, tirx.Var]] = None, # noqa: UP006
) -> relax.Type:
if undefined_var_remap is None:
undefined_var_remap = self.undefined_shape_vars_remap
if isinstance(struct_info, relax.TensorType):
return (
relax.TensorType(
self._update_shape(struct_info.shape.values, undefined_var_remap),
struct_info.dtype,
)
if struct_info.shape is not None and isinstance(struct_info.shape, relax.ShapeExpr)
else struct_info
)
if isinstance(struct_info, relax.ShapeType):
return (
relax.ShapeType(self._update_shape(struct_info.values, undefined_var_remap))
if struct_info.values is not None
else struct_info
)
if isinstance(struct_info, relax.ObjectType):
return relax.ObjectType()
if isinstance(struct_info, relax.TupleType):
return relax.TupleType(
[self._update_struct_info(field_sinfo) for field_sinfo in struct_info.fields]
)
return struct_info
def _copy_undefined_var(
self,
expr: tirx.Expr,
undefined_var_remap: Dict[tirx.Var, tirx.Var], # noqa: UP006
) -> None:
def _visit_expr(e: tirx.Expr) -> None:
if isinstance(e, tirx.Var) and e not in undefined_var_remap:
new_var = tirx.Var(e.name, e.ty)
undefined_var_remap[e] = new_var
tirx.stmt_functor.post_order_visit(expr, _visit_expr)
def _update_shape(
self,
shape: List[tirx.Expr], # noqa: UP006
undefined_var_remap: Dict[tirx.Var, tirx.Var], # noqa: UP006
) -> List[tirx.Expr]: # noqa: UP006
new_shape = []
for v in shape:
self._copy_undefined_var(v, undefined_var_remap)
new_shape.append(tirx.stmt_functor.substitute(v, undefined_var_remap))
return new_shape
def _extract_pipeline_stages(
func: relax.Function,
) -> Tuple[List[List[relax.Binding]], List[List[relax.Var]], List[List[relax.Var]]]: # noqa: UP006
pipeline_stages: List[List[relax.Binding]] = [] # noqa: UP006
stage_send_vars: List[List[relax.Var]] = [] # noqa: UP006
stage_receive_vars: List[List[relax.Var]] = [] # noqa: UP006
# Requiring that the function has only one body block which is a dataflow block
assert isinstance(func.body, relax.SeqExpr)
assert len(func.body.blocks) == 1
assert isinstance(func.body.blocks[0], relax.DataflowBlock)
bindings = func.body.blocks[0].bindings
boundary_var = None
current_stage_bindings: List[relax.Binding] = [] # noqa: UP006
current_stage_receive_vars: List[relax.Var] = [] # noqa: UP006
for binding in bindings:
if (
isinstance(binding, relax.VarBinding)
and isinstance(binding.value, relax.Call)
and binding.value.op == tvm.ir.Op.get("relax.call_pure_packed")
and binding.value.args[0].global_symbol == "mlc.pipeline_parallel_stage_boundary"
):
assert len(current_stage_bindings) > 0
pipeline_stages.append(current_stage_bindings)
assert all(receive_var is not None for receive_var in current_stage_receive_vars)
stage_receive_vars.append(current_stage_receive_vars)
args = binding.value.args[1:]
assert len(args) >= 1 and all(isinstance(arg, relax.Var) for arg in args)
stage_send_vars.append(list(args))
boundary_var = binding.var
current_stage_bindings = []
current_stage_receive_vars = [boundary_var] if len(args) == 1 else [None for _ in args]
elif (
isinstance(binding, relax.VarBinding)
and isinstance(binding.value, relax.TupleGetItem)
and binding.value.tuple_value.same_as(boundary_var)
):
current_stage_receive_vars[binding.value.index] = binding.var
else:
current_stage_bindings.append(binding)
assert len(current_stage_bindings) > 0
pipeline_stages.append(current_stage_bindings)
assert all(receive_var is not None for receive_var in current_stage_receive_vars)
stage_receive_vars.append(current_stage_receive_vars)
stage_send_vars.append([])
return pipeline_stages, stage_send_vars, stage_receive_vars
def _analyze_required_func_params(
pipeline_stages: List[List[relax.Binding]], # noqa: UP006
func_params: List[relax.Var], # noqa: UP006
) -> List[List[relax.Var]]: # noqa: UP006
analyzer = _RequiredFuncParamAnalyzer(func_params)
required_func_params: List[List[relax.Var]] = [] # noqa: UP006
for stage_bindings in pipeline_stages:
required_params: List[relax.Var] # noqa: UP006
required_params = analyzer.run(stage_bindings)
required_func_params.append(required_params)
return required_func_params
@visitor
class _RequiredFuncParamAnalyzer(PyExprVisitor):
"""The IR visitor which analyzes the required func parameters in each pipeline stage."""
def __init__(self, func_params: List[relax.Var]) -> None: # noqa: UP006
self.func_params = set(func_params)
self.required_params: List[relax.Var] # noqa: UP006
def run(self, stage_bindings: List[relax.Binding]) -> List[relax.Var]: # noqa: UP006
"""Entry point of the visitor."""
self.required_params = []
for binding in stage_bindings:
self.visit_binding(binding)
return self.required_params
def visit_var_(self, var: relax.Var) -> None:
if var in self.func_params:
if var not in self.required_params:
self.required_params.append(var)
@@ -0,0 +1,49 @@
"""A compiler pass that scatters TupleGetItem for lazy TupleGetItems."""
from typing import Dict # noqa: UP035
import tvm
from tvm import relax
from tvm.ir.module import IRModule
from tvm.relax.analysis import remove_all_unused
from tvm.relax.expr import Expr, Var
from tvm.relax.expr_functor import PyExprMutator, mutator
@tvm.transform.module_pass(opt_level=0, name="ScatterTupleGetItem")
class ScatterTupleGetItem:
"""A compiler pass that scatters TupleGetItem for lazy TupleGetItems."""
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
return _Scatter(mod).transform()
@mutator
class _Scatter(PyExprMutator):
def __init__(self, mod: IRModule) -> None:
super().__init__(mod)
self.mod = mod
self.var_map: Dict[Var, Expr] = {} # noqa: UP006
def transform(self) -> IRModule:
"""Entry point"""
for g_var, func in self.mod.functions_items():
if isinstance(func, relax.Function):
updated_func = self.visit_expr(func)
updated_func = remove_all_unused(updated_func)
self.builder_.update_func(g_var, updated_func)
return self.builder_.get()
def visit_var_binding_(self, binding: relax.VarBinding):
super().visit_var_binding_(binding)
if isinstance(binding.value, relax.TupleGetItem):
self.var_map[binding.var] = binding.value
def visit_dataflow_var_(self, var: relax.DataflowVar) -> Expr:
if var in self.var_map:
new_var = self.builder_.emit(self.var_map[var], name_hint=var.name_hint)
self.set_var_remap(var, new_var)
self.var_map.pop(var)
return new_var
return var