/*! * Copyright (c) 2023-2025 by Contributors * \file serve/sampler/cpu_sampler.cc * \brief The implementation for CPU sampler functions. */ #include #include #include #include #include "../../support/random.h" #include "../../support/threading_backend.h" #include "sampler.h" namespace mlc { namespace llm { namespace serve { TVM_FFI_STATIC_INIT_BLOCK() { SamplerObj::RegisterReflection(); } /*! * \brief Sample a value from the input probability distribution with top-p. * The input is a batch of distributions, and we use `unit_offset` to specify * which distribution to sample from. * \param prob The input batch of probability distributions. * \param unit_offset The offset specifying which distribution to output * \param input_prob_offset The offset specifying which distribution to sample from. * \param top_p The top-p value of sampling. * \param uniform_sample The random number in [0, 1] for sampling. * \return The sampled value and probability. * \note This function is an enhancement of SampleTopPFromProb in TVM Unity. * We will upstream the enhancement after it gets stable. */ TokenProbPair SampleTopPFromProb(Tensor prob, int unit_offset, int input_prob_offset, double top_p, double uniform_sample) { // prob: (*, v) // The prob array may have arbitrary ndim and shape. // The last dimension corresponds to the prob distribution size. // We use the `unit_offset` parameter to determine which slice // of the prob array we sample from. TVM_FFI_ICHECK(prob.IsContiguous()); TVM_FFI_ICHECK(prob.DataType() == (DLDataType{kDLFloat, 32, 1})); TVM_FFI_ICHECK_EQ(prob->device.device_type, DLDeviceType::kDLCPU); int64_t ndata = prob->shape[prob->ndim - 1]; const float* __restrict p_prob = static_cast(__builtin_assume_aligned(prob->data, 4)) + (input_prob_offset * ndata); constexpr double one = 1.0f - 1e-5f; if (top_p == 0) { // Specially handle case where top_p == 0. // This case is equivalent to doing argmax. int argmax_pos = -1; float max_prob = 0.0; float sum_prob = 0.0; for (int i = 0; i < ndata; ++i) { if (p_prob[i] > max_prob) { max_prob = p_prob[i]; argmax_pos = i; } // Early exit. sum_prob += p_prob[i]; if (1 - sum_prob <= max_prob) { break; } } return {argmax_pos, 1.0}; } if (top_p >= one) { // Specially handle case where top_p == 1. double prob_sum = 0.0f; for (int64_t i = 0; i < ndata; ++i) { prob_sum += p_prob[i]; if (prob_sum >= uniform_sample) { return {i, p_prob[i]}; } } TVM_FFI_ICHECK(false) << "Possibly prob distribution contains NAN."; } // Key observation: when we are doing top_p sampling // usually we only need to preserve some of the elements with // high probabilities before we do sort thread_local std::vector> data; auto sample_top_p_with_filter = [&](float cuttoff) -> std::pair { data.clear(); // filter the data with cuttoff float cutoff_sum = 0.0f; for (int64_t i = 0; i < ndata; ++i) { if (p_prob[i] >= cuttoff) { cutoff_sum += p_prob[i]; data.emplace_back(std::make_pair(p_prob[i], static_cast(i))); if (cutoff_sum > 1 - cuttoff) { // Short cut. When the remaining parts cannot have total // probability larger than cutoff, we can quit. break; } } } if (data.size() == 0) return std::make_pair(-1, -1); auto fcmp = [](const std::pair& lhs, const std::pair& rhs) { return lhs.first > rhs.first; }; std::sort(data.begin(), data.end(), fcmp); // short cut, if we know that // uniform sample < p[0] / top_p // we know that unform_sample < p[0] / top_p_sum // because top_p_sum guarantees to be smaller than top_p // so we can simply return the argmax sample // without computing anything if (uniform_sample < data[0].first / top_p) { return std::make_pair(data[0].first, data[0].second); } // compute top_p_sum float cum_sum_prob = 0.0f; float top_p_sum = 0.0f; for (auto it = data.begin(); it != data.end(); ++it) { float prob = it->first; if (cum_sum_prob < top_p) { top_p_sum += prob; } else { // we get to the right cutoff pt break; } cum_sum_prob += prob; it->first = cum_sum_prob; } // we find that the current total sum by the given cutoff // is not sufficient to cover everything // this means we might need to retry a smaller cutoff pt. if (cum_sum_prob < top_p && cuttoff != 0.0f) return std::make_pair(-1, -1); float last_cum_sum_prob = 0.0; for (auto it = data.begin(); it != data.end(); ++it) { if (uniform_sample < it->first / top_p_sum) { return std::make_pair(it->first - last_cum_sum_prob, it->second); } last_cum_sum_prob = it->first; } return std::make_pair(data[static_cast(data.size()) - 1].first - last_cum_sum_prob, data[static_cast(data.size()) - 1].second); }; if (top_p < 1) { // sample through cutoff by a number // by pigeonhole principle we will get at most 1024 elements // usually it is much less by applying this filtering(order of 10 - 20) data.reserve(256); std::pair sampled_index = sample_top_p_with_filter(top_p / 1024); if (sampled_index.second >= 0) return {sampled_index.second, sampled_index.first}; } // fallback via full prob, rare case data.reserve(ndata); std::pair sampled_index = sample_top_p_with_filter(0.0f); TVM_FFI_ICHECK_GE(sampled_index.second, 0); return {sampled_index.second, sampled_index.first}; } /*! * \brief Renormalize the probability distribution by the top p value. * \param prob The input batch of probability distributions. * \param unit_offset The offset specifying which distribution to output * \param top_p The top p value for renormalization. * \param eps A small epsilon value for comparison stability. */ void RenormalizeProbByTopP(Tensor prob, int unit_offset, double top_p, double eps) { // prob: (*, v) // The prob array may have arbitrary ndim and shape. // The last dimension corresponds to the prob distribution size. // We use the `unit_offset` parameter to determine which slice // of the prob array we will renormalize. TVM_FFI_ICHECK(prob.IsContiguous()); TVM_FFI_ICHECK(prob.DataType() == (DLDataType{kDLFloat, 32, 1})); TVM_FFI_ICHECK_EQ(prob->device.device_type, DLDeviceType::kDLCPU); if (top_p == 1.0) { // No renormalization is needed if top_p is 1. return; } int vocab_size = prob->shape[prob->ndim - 1]; float* __restrict p_prob = static_cast(__builtin_assume_aligned(prob->data, 4)) + (unit_offset * vocab_size); // We manually choice the cutoff values of "top_p / 256" and "top_p / 8192". // In most of the cases, only one round is needed. std::vector cutoff_values{top_p / 256, top_p / 8192, 0.0f}; // Create the upper partition vector and the lower partition rolling vectors. std::vector upper_partition; std::vector lower_partitions[2]; upper_partition.reserve(vocab_size); lower_partitions[0].reserve(vocab_size); lower_partitions[1].reserve(vocab_size); float upper_partition_sum = 0.0; for (int round = 0; round < static_cast(cutoff_values.size()); ++round) { const float* lower_partition_begin; const float* lower_partition_end; if (round == 0) { lower_partition_begin = p_prob; lower_partition_end = p_prob + vocab_size; } else { int idx = (round - 1) & 1; lower_partition_begin = lower_partitions[idx].data(); lower_partition_end = lower_partitions[idx].data() + lower_partitions[idx].size(); } // - Partition the last round lower partition into upper and lower // based on the new cutoff value. std::vector& lower_partition = lower_partitions[round & 1]; lower_partition.clear(); for (const float* ptr = lower_partition_begin; ptr != lower_partition_end; ++ptr) { if (*ptr >= cutoff_values[round]) { upper_partition.push_back(*ptr); upper_partition_sum += *ptr; } else { lower_partition.push_back(*ptr); } } // - If the upper partition sum is at least top p, exit the loop. if (upper_partition_sum >= top_p - eps) { break; } } // - Sort the upper partition in descending order. std::sort(upper_partition.begin(), upper_partition.end(), std::greater<>()); // - Find the top p boundary prob value. float boundary_value = -1.0; upper_partition_sum = 0.0; for (float upper_value : upper_partition) { upper_partition_sum += upper_value; if (upper_partition_sum >= top_p - eps) { boundary_value = upper_value; break; } } // - Mask all values smaller than the boundary to 0. float renormalize_sum = 0.0; std::vector upper_partition_indices; upper_partition_indices.reserve(vocab_size); for (int i = 0; i < vocab_size; ++i) { if (p_prob[i] >= boundary_value) { upper_partition_indices.push_back(i); renormalize_sum += p_prob[i]; } else { p_prob[i] = 0.0; } } // - Renormalize. for (int idx : upper_partition_indices) { p_prob[idx] /= renormalize_sum; } } namespace detail { /*! \brief Implementation of getting top probs on CPU. */ template std::vector ComputeTopProbsImpl(const float* p_prob, int ndata) { std::vector top_probs; top_probs.reserve(num_top_probs); for (int i = 0; i < num_top_probs; ++i) { top_probs.emplace_back(-1, -1.0f); } float sum_prob = 0.0; // Selection argsort. for (int p = 0; p < ndata; ++p) { int i = num_top_probs - 1; for (; i >= 0; --i) { if (p_prob[p] > top_probs[i].second) { if (i != num_top_probs - 1) { top_probs[i + 1] = top_probs[i]; } } else { break; } } if (i != num_top_probs - 1) { top_probs[i + 1] = {p, p_prob[p]}; } // Early exit. sum_prob += p_prob[p]; if (1 - sum_prob <= top_probs[num_top_probs - 1].second) { break; } } return top_probs; } } // namespace detail /*! \brief Get the probs of a few number of tokens with top probabilities. */ inline std::vector ComputeTopProbs(Tensor prob, int unit_offset, int num_top_probs) { TVM_FFI_ICHECK_LE(num_top_probs, 5); TVM_FFI_ICHECK_EQ(prob->ndim, 2); int ndata = prob->shape[1]; const float* __restrict p_prob = static_cast(__builtin_assume_aligned(prob->data, 4)) + (unit_offset * ndata); switch (num_top_probs) { case 0: return {}; case 1: return detail::ComputeTopProbsImpl<1>(p_prob, ndata); case 2: return detail::ComputeTopProbsImpl<2>(p_prob, ndata); case 3: return detail::ComputeTopProbsImpl<3>(p_prob, ndata); case 4: return detail::ComputeTopProbsImpl<4>(p_prob, ndata); case 5: return detail::ComputeTopProbsImpl<5>(p_prob, ndata); } throw; } /********************* CPU Sampler *********************/ class CPUSampler : public SamplerObj { public: explicit CPUSampler(Optional trace_recorder) : trace_recorder_(std::move(trace_recorder)) {} Tensor BatchRenormalizeProbsByTopP(Tensor probs_on_device, // const std::vector& sample_indices, // const Array& request_ids, // const Array& generation_cfg) final { // probs_on_device: (n, v) TVM_FFI_ICHECK_EQ(probs_on_device->ndim, 2); // - Copy probs to CPU RECORD_EVENT(trace_recorder_, request_ids, "start copy probs to CPU"); Tensor probs_on_host = CopyProbsToCPU(probs_on_device); RECORD_EVENT(trace_recorder_, request_ids, "finish copy probs to CPU"); int num_samples = sample_indices.size(); int num_probs = probs_on_device->shape[0]; int vocab_size = probs_on_device->shape[1]; TVM_FFI_ICHECK_EQ(request_ids.size(), num_samples); TVM_FFI_ICHECK_EQ(generation_cfg.size(), num_samples); std::vector top_p_indices; std::vector top_p_values; for (int i = 0; i < num_samples; ++i) { if (top_p_indices.empty() || top_p_indices.back() != sample_indices[i]) { top_p_indices.push_back(sample_indices[i]); top_p_values.push_back(generation_cfg[i]->top_p); } else { TVM_FFI_ICHECK(fabs(top_p_values.back() - generation_cfg[i]->top_p) < eps_) << "Sampler requires the top_p values for each prob distribution are the same."; } } if (top_p_indices.empty()) { // Return if no top p needs to apply. return probs_on_host; } tvm::runtime::parallel_for_with_threading_backend( [this, &probs_on_host, &request_ids, &top_p_indices, &top_p_values](int i) { RECORD_EVENT(this->trace_recorder_, request_ids[i], "start renormalize by top p"); RenormalizeProbByTopP(probs_on_host, top_p_indices[i], top_p_values[i], eps_); RECORD_EVENT(this->trace_recorder_, request_ids[i], "finish renormalize by top p"); }, 0, static_cast(top_p_indices.size())); return probs_on_host; } std::vector BatchSampleTokensWithProbBeforeTopP( Tensor probs_on_device, // const std::vector& sample_indices, // const Array& request_ids, // const Array& generation_cfg, // const std::vector& rngs) final { // probs_on_device: (n, v) TVM_FFI_ICHECK_EQ(probs_on_device->ndim, 2); // - Copy probs to CPU RECORD_EVENT(trace_recorder_, request_ids, "start copy probs to CPU"); Tensor probs_on_host = CopyProbsToCPU(probs_on_device); RECORD_EVENT(trace_recorder_, request_ids, "finish copy probs to CPU"); return BatchSampleTokensImpl(probs_on_host, sample_indices, request_ids, generation_cfg, rngs, /*top_p_applied=*/false); } std::vector BatchSampleTokensWithProbAfterTopP( Tensor probs_on_host, // const std::vector& sample_indices, // const Array& request_ids, // const Array& generation_cfg, // const std::vector& rngs) final { return BatchSampleTokensImpl(probs_on_host, sample_indices, request_ids, generation_cfg, rngs, /*top_p_applied=*/true); } std::pair>, std::vector> BatchVerifyDraftTokensWithProbAfterTopP( Tensor probs_on_host, const Array& request_ids, const std::vector& cum_verify_lengths, const Array& generation_cfg, const std::vector& rngs, const std::vector>& draft_output_tokens, const std::vector& token_tree_parent_ptr, Tensor draft_probs_on_device) final { // probs_on_host: (n, v) RECORD_EVENT(trace_recorder_, request_ids, "start draft verification"); TVM_FFI_ICHECK_EQ(probs_on_host->ndim, 2); int num_sequence = static_cast(cum_verify_lengths.size()) - 1; TVM_FFI_ICHECK_EQ(rngs.size(), num_sequence); TVM_FFI_ICHECK_EQ(draft_output_tokens.size(), num_sequence); Tensor draft_probs_on_host = draft_probs_on_device.CopyTo(DLDevice{kDLCPU, 0}); std::vector> sample_results; sample_results.resize(num_sequence); float* __restrict global_p_probs = static_cast(__builtin_assume_aligned(probs_on_host->data, 4)); int vocab_size = probs_on_host->shape[1]; std::vector last_accepted_tree_node(num_sequence, 0); tvm::runtime::parallel_for_with_threading_backend( [&](int i) { int verify_start = cum_verify_lengths[i]; int verify_end = cum_verify_lengths[i + 1]; TVM_FFI_ICHECK_EQ(token_tree_parent_ptr[verify_start], -1); for (int j = verify_start + 1; j < verify_end; ++j) { TVM_FFI_ICHECK_EQ(token_tree_parent_ptr[j], j - verify_start - 1) << "CPU sampler only supports chain-style draft tokens."; } int cur_token_idx = 0; // Sub 1 to ignore the last prediction. for (; cur_token_idx < verify_end - verify_start - 1; ++cur_token_idx) { float* p_probs = global_p_probs + (verify_start + cur_token_idx) * vocab_size; int cur_token = draft_output_tokens[i][cur_token_idx].GetTokenId(); float q_value = draft_output_tokens[i][cur_token_idx].sampled_token_id.second; float p_value = p_probs[cur_token]; if (p_value >= q_value) { sample_results[i].push_back( SampleResult{{cur_token, p_value}, ComputeTopProbs(probs_on_host, verify_start + cur_token_idx, generation_cfg[i]->top_logprobs)}); continue; } float r = rngs[i]->GetRandomNumber(); if (r < p_value / (q_value + eps_)) { sample_results[i].push_back( SampleResult{{cur_token, p_value}, ComputeTopProbs(probs_on_host, verify_start + cur_token_idx, generation_cfg[i]->top_logprobs)}); continue; } // normalize a new probability distribution double sum_v = 0.0; const float* __restrict p_qdist = static_cast(__builtin_assume_aligned(draft_probs_on_host->data, 4)) + (verify_start + cur_token_idx + 1) * vocab_size; for (int j = 0; j < vocab_size; ++j) { p_probs[j] = std::max(p_probs[j] - p_qdist[j], 0.0f); sum_v += p_probs[j]; } for (int j = 0; j < vocab_size; ++j) { p_probs[j] /= sum_v; } // sample a new token from the new distribution SampleResult sample_result; sample_result.sampled_token_id = SampleTopPFromProb( probs_on_host, verify_start + cur_token_idx, verify_start + cur_token_idx, /*top_p=*/1.0f, rngs[i]->GetRandomNumber()); sample_result.top_prob_tokens = ComputeTopProbs( probs_on_host, verify_start + cur_token_idx, generation_cfg[i]->top_logprobs); sample_results[i].push_back(sample_result); break; } last_accepted_tree_node[i] = cur_token_idx; // if cur_token_idx == verify_end - verify_start - 1 // all draft tokens are accepted // we sample a new token if (cur_token_idx == verify_end - verify_start - 1) { SampleResult sample_result; // sample a new token from the original distribution sample_result.sampled_token_id = SampleTopPFromProb( probs_on_host, verify_start + cur_token_idx, verify_start + cur_token_idx, /*top_p=*/1.0f, rngs[i]->GetRandomNumber()); sample_result.top_prob_tokens = ComputeTopProbs( probs_on_host, verify_start + cur_token_idx, generation_cfg[i]->top_logprobs); sample_results[i].push_back(sample_result); } }, 0, num_sequence); RECORD_EVENT(trace_recorder_, request_ids, "finish draft verification"); return {sample_results, last_accepted_tree_node}; } private: std::vector BatchSampleTokensImpl(Tensor probs_on_host, // const std::vector& sample_indices, // const Array& request_ids, // const Array& generation_cfg, // const std::vector& rngs, // bool top_p_applied) { // probs_on_host: (n, v) RECORD_EVENT(trace_recorder_, request_ids, "start sampling"); TVM_FFI_ICHECK_EQ(probs_on_host->ndim, 2); TVM_FFI_ICHECK_EQ(probs_on_host->device.device_type, DLDeviceType::kDLCPU); // - Sample tokens from probabilities. int n = request_ids.size(); TVM_FFI_ICHECK_EQ(generation_cfg.size(), n); TVM_FFI_ICHECK_EQ(rngs.size(), n); std::vector sample_results; sample_results.resize(n); tvm::runtime::parallel_for_with_threading_backend( [this, &sample_results, &probs_on_host, &generation_cfg, &rngs, &request_ids, top_p_applied, sample_indices](int i) { RECORD_EVENT(this->trace_recorder_, request_ids[i], "start sample token"); // Sample top p from probability. double top_p = top_p_applied ? 1.0f : (generation_cfg[i]->temperature < eps_ ? 0.0 : generation_cfg[i]->top_p); sample_results[i].sampled_token_id = SampleTopPFromProb( probs_on_host, i, sample_indices[i], top_p, rngs[i]->GetRandomNumber()); sample_results[i].top_prob_tokens = ComputeTopProbs(probs_on_host, i, generation_cfg[i]->top_logprobs); RECORD_EVENT(this->trace_recorder_, request_ids[i], "finish sample token"); }, 0, n); RECORD_EVENT(trace_recorder_, request_ids, "finish sampling"); return sample_results; } /*! \brief Copy prob distributions from device to CPU. */ Tensor CopyProbsToCPU(Tensor probs_on_device) { // probs_on_device: (n, v) if (probs_on_device->device.device_type == kDLCPU) { return probs_on_device; } TVM_FFI_ICHECK(probs_on_device->device.device_type != kDLCPU); if (probs_host_.defined()) { TVM_FFI_ICHECK_EQ(probs_host_->shape[1], probs_on_device->shape[1]); } int64_t init_size = probs_host_.defined() ? probs_host_->shape[0] : 32; int64_t num_tokens = probs_on_device->shape[0]; int64_t vocab_size = probs_on_device->shape[1]; while (init_size < num_tokens) { init_size *= 2; } if (!probs_host_.defined() || init_size != probs_host_->shape[0]) { probs_host_ = Tensor::Empty({init_size, vocab_size}, probs_on_device->dtype, DLDevice{kDLCPU, 0}); } TVM_FFI_ICHECK_LE(num_tokens, probs_host_->shape[0]); Tensor view = probs_host_.CreateView({num_tokens, vocab_size}, probs_on_device->dtype); view.CopyFrom(probs_on_device); return view; } /*! \brief The event trace recorder for requests. */ Optional trace_recorder_; /*! \brief Customized function which computes prob distribution from logits */ Function flogits_to_probs_inplace_; /*! \brief Probability distribution array on CPU. */ Tensor probs_host_{nullptr}; const float eps_ = 1e-5; }; Sampler Sampler::CreateCPUSampler(Optional trace_recorder) { return Sampler(tvm::ffi::make_object(std::move(trace_recorder))); } } // namespace serve } // namespace llm } // namespace mlc