Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

176 lines
8.4 KiB
Python

"""Operators for batch verify in speculative decoding."""
from tvm.script import tirx as T
# mypy: disable-error-code="attr-defined,valid-type,name-defined"
def batch_spec_verify(vocab_size):
"""Batch draft verify function. This function verifies the token tree.
Before calling the function
- token_tree_parent_ptr[b] should store the root of the tree
- draft_probs[node_id, :] stores the prob that samples the correspond tree node
- model_probs[node_id, :] stores the prob that should be used to sample its children
- Please note that the storage convention difference between model_probs and draft_probs
draft_probs was stored on the token node, while model_probs stores on the parent.
This is an intentional design since we can sample different child token with different
proposal draft probabilities, but the ground truth model_prob is unique per parent.
After calling the function
- token_tree_parent_ptr[b] points to the last token accepted
- There should be a followup sample step that samples from model_probs[token_tree_parent_ptr[b], :]
This token will be appended to the token generated.
This function will inplace update model_probs if a token was rejected and renormalization is needed.
Parameters
----------
draft_probs:
The draft probability attached to each tree node
draft_tokens:
The draft token in each node
model_probs:
The model proability attached to each parent
token_tree_first_child:
The first child of each tree node, if there is no child, it should be -1
token_tree_next_sibling
The next sibling of each tree node, if there is no next sibling, it should be -1
uniform_samples
Per node uniform sample used to check rejection
token_tree_parent_ptr:
Current parent ptr state
""" # noqa: E501
TX = 1024
def _var(dtype="int32"):
return T.sblock_alloc_buffer((1,), dtype, scope="local")
# fmt: off
@T.prim_func(private=True, s_tir=True)
def _func(
var_draft_probs: T.handle,
var_draft_tokens: T.handle,
var_model_probs: T.handle,
var_token_tree_first_child: T.handle,
var_token_tree_next_sibling: T.handle,
var_uniform_samples: T.handle,
var_token_tree_parent_ptr: T.handle,
):
"""
[
blockIdx.x on batch,
threadIdx.x on vocab_size,
for loop over excessive amounts
]
"""
T.func_attr({"tirx.is_scheduled": 1, "tirx.noalias": True})
num_nodes = T.int32()
nbatch = T.int32()
draft_probs = T.match_buffer(var_draft_probs, (num_nodes, vocab_size), "float32")
draft_tokens = T.match_buffer(var_draft_tokens, (num_nodes,), "int32")
model_probs = T.match_buffer(var_model_probs, (num_nodes, vocab_size), "float32")
token_tree_first_child = T.match_buffer(var_token_tree_first_child, (num_nodes,), "int32")
token_tree_next_sibling = T.match_buffer(var_token_tree_next_sibling, (num_nodes,), "int32")
uniform_samples = T.match_buffer(var_uniform_samples, (num_nodes,), "float32")
token_tree_parent_ptr = T.match_buffer(var_token_tree_parent_ptr, (nbatch,), "int32")
with T.sblock("kernel"):
child_ptr = _var()
parent_ptr = _var()
child_token = _var()
done = _var("bool")
psum = _var("float32")
t0 = _var("float32")
model_prob_local = _var("float32")
draft_prob_local = _var("float32")
p_child = _var("float32")
q_child = _var("float32")
uniform_sample = _var("float32")
pred_shared = T.sblock_alloc_buffer((1,), "bool", scope="shared")
pred_local = T.sblock_alloc_buffer((1,), "bool", scope="local")
for _bx in T.thread_binding(0, nbatch, thread="blockIdx.x"):
for _tx in T.thread_binding(0, TX, thread="threadIdx.x"):
with T.sblock("CTA"):
# batch size
b = T.axis.S(nbatch, _bx)
tx = T.axis.S(TX, _tx)
parent_ptr[0] = token_tree_parent_ptr[b]
child_ptr[0] = token_tree_first_child[parent_ptr[0]]
done[0] = False
while T.Not(done[0]):
T.tvm_storage_sync("shared") # ensure all effects last round are visible
if child_ptr[0] == -1:
done[0] = True
T.tvm_storage_sync("shared") # sync before exit
else:
# decide to validate current ptr
if tx == 0:
child_token[0] = draft_tokens[child_ptr[0]]
p_child[0] = model_probs[parent_ptr[0], child_token[0]]
q_child[0] = draft_probs[child_ptr[0], child_token[0]]
uniform_sample[0] = uniform_samples[child_ptr[0]]
pred_shared[0] = p_child[0] >= uniform_sample[0] * q_child[0] # use multiplication to avoid division by zero # noqa: E501
T.tvm_storage_sync("shared") # make sure all read of model_probs are done # noqa: E501
pred_local[0] = pred_shared[0]
# accept the proposal, we move to child
if pred_local[0]:
parent_ptr[0] = child_ptr[0]
child_ptr[0] = token_tree_first_child[child_ptr[0]]
else:
psum[0] = 0.0
# renormalize probability, predicated by stopped_expansion[b]:
for i in T.serial(T.ceildiv(vocab_size, TX)):
k = T.meta_var(i * TX + tx)
if k < vocab_size:
model_prob_local[0] = model_probs[parent_ptr[0], k]
draft_prob_local[0] = draft_probs[child_ptr[0], k]
model_prob_local[0] = T.max(model_prob_local[0] - draft_prob_local[0], 0.0) # noqa: E501
psum[0] += model_prob_local[0]
with T.sblock("block_cross_thread"):
T.reads(psum[0])
T.writes(t0[0])
T.attr(
T.comm_reducer(lambda x0, y0: x0 + y0, [T.float32(0)]),
"reduce_scope",
T.int32(0),
)
T.tvm_thread_allreduce(T.uint32(1), psum[0], True, t0[0], tx, dtype="void") # noqa: E501
if t0[0] < 1e-7:
# accept the proposal, we move to child
parent_ptr[0] = child_ptr[0]
child_ptr[0] = token_tree_first_child[child_ptr[0]]
else:
# renormalize
for i in T.serial(T.ceildiv(vocab_size, TX)):
k = T.meta_var(i * TX + tx)
if k < vocab_size:
model_prob_local[0] = model_probs[parent_ptr[0], k]
draft_prob_local[0] = draft_probs[child_ptr[0], k]
model_prob_local[0] = T.max(model_prob_local[0] - draft_prob_local[0], 0.0) # noqa: E501
model_probs[parent_ptr[0], k] = model_prob_local[0] / t0[0] # noqa: E501
child_ptr[0] = token_tree_next_sibling[child_ptr[0]]
if tx == 0:
token_tree_parent_ptr[b] = parent_ptr[0]
# fmt: on
return _func