"""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