#include "engine/framework/sampling/hf_sampler.h" #include "test_assert.h" #include #include #include #include #include #include #include #include #include #include #include namespace { using engine::sampling::HfSampler; using engine::sampling::HfSamplerScratch; using engine::sampling::HfSamplingOptions; using engine::sampling::HfTorchSamplingState; using engine::sampling::TorchCudaSamplingPolicy; std::vector make_logits(size_t vocab_size, int step) { std::vector logits(vocab_size); for (size_t index = 0; index < vocab_size; ++index) { const double x = static_cast(index + 1) * 0.013 + static_cast(step) * 0.071; logits[index] = static_cast(std::sin(x) * 3.0 + std::cos(x * 0.37) * 1.5); } return logits; } std::vector make_history(size_t vocab_size, int step) { std::vector history; history.reserve(80); for (int index = 0; index < 80; ++index) { history.push_back(static_cast((index * 37 + step * 11) % static_cast(vocab_size))); } return history; } int32_t reference_argmax(const std::vector & scores) { size_t best = 0; for (size_t index = 1; index < scores.size(); ++index) { if (scores[index] > scores[best]) { best = index; } } return static_cast(best); } void reference_repetition_penalty( std::vector & scores, const std::vector & history, float penalty) { if (penalty == 1.0F) { return; } std::unordered_set seen; for (const int32_t token : history) { if (token < 0 || static_cast(token) >= scores.size()) { continue; } if (!seen.insert(token).second) { continue; } float & score = scores[static_cast(token)]; score = score < 0.0F ? score * penalty : score / penalty; } } void reference_top_k(std::vector & scores, int64_t top_k, int64_t min_tokens_to_keep) { if (top_k <= 0 || scores.empty()) { return; } const int64_t keep = std::min( static_cast(scores.size()), std::max(top_k, min_tokens_to_keep)); if (keep >= static_cast(scores.size())) { return; } std::vector order(scores.size()); for (size_t index = 0; index < scores.size(); ++index) { order[index] = static_cast(index); } auto kth = order.begin() + static_cast(keep - 1); std::nth_element(order.begin(), kth, order.end(), [&](int32_t lhs, int32_t rhs) { return scores[static_cast(lhs)] > scores[static_cast(rhs)]; }); const float threshold = scores[static_cast(*kth)]; for (float & score : scores) { if (score < threshold) { score = -std::numeric_limits::infinity(); } } } void reference_top_p(std::vector & scores, float top_p, int64_t min_tokens_to_keep) { if (!(top_p < 1.0F) || scores.empty()) { return; } std::vector sorted; sorted.reserve(scores.size()); for (size_t index = 0; index < scores.size(); ++index) { if (std::isfinite(scores[index])) { sorted.push_back(static_cast(index)); } } if (sorted.empty()) { return; } std::sort(sorted.begin(), sorted.end(), [&](int32_t lhs, int32_t rhs) { const float lhs_score = scores[static_cast(lhs)]; const float rhs_score = scores[static_cast(rhs)]; if (lhs_score == rhs_score) { return lhs < rhs; } return lhs_score < rhs_score; }); const float max_score = scores[static_cast(sorted.back())]; std::vector weights(sorted.size()); double total = 0.0; for (size_t index = 0; index < sorted.size(); ++index) { weights[index] = std::exp(static_cast(scores[static_cast(sorted[index])] - max_score)); total += weights[index]; } double cumulative = 0.0; const double remove_mass = 1.0 - static_cast(top_p); const size_t min_keep = std::min(static_cast(min_tokens_to_keep), sorted.size()); const size_t protected_from = sorted.size() - min_keep; for (size_t index = 0; index < sorted.size(); ++index) { cumulative += weights[index] / total; if (index < protected_from && cumulative <= remove_mass) { scores[static_cast(sorted[index])] = -std::numeric_limits::infinity(); } } } int32_t reference_sample( const std::vector & logits, const std::vector & history, const HfSamplingOptions & options, std::mt19937 & fallback_rng, const HfTorchSamplingState * torch_state) { std::vector scores = logits; reference_repetition_penalty(scores, history, options.repetition_penalty); if (!options.do_sample) { return reference_argmax(scores); } if (options.temperature != 1.0F) { for (float & score : scores) { score /= options.temperature; } } reference_top_k(scores, options.top_k, options.min_tokens_to_keep); reference_top_p(scores, options.top_p, options.min_tokens_to_keep); std::vector candidates; std::vector weights; candidates.reserve(scores.size()); float max_score = -std::numeric_limits::infinity(); for (size_t index = 0; index < scores.size(); ++index) { if (std::isfinite(scores[index])) { candidates.push_back(static_cast(index)); max_score = std::max(max_score, scores[index]); } } weights.reserve(candidates.size()); for (const int32_t token : candidates) { weights.push_back(std::exp(static_cast(scores[static_cast(token)] - max_score))); } if (torch_state != nullptr && torch_state->policy != nullptr && torch_state->policy->cuda_fast_path) { double best_rank = -std::numeric_limits::infinity(); int32_t best_token = -1; for (size_t index = 0; index < candidates.size(); ++index) { const int32_t token = candidates[index]; const float exponential = engine::sampling::torch_cuda_tensor_iterator_exponential_element( torch_state->seed, static_cast(scores.size()), static_cast(token), torch_state->call_index, torch_state->policy->multiprocessor_count, torch_state->policy->max_threads_per_multiprocessor); const double rank = weights[index] / static_cast(exponential); if (rank > best_rank) { best_rank = rank; best_token = token; } } return best_token; } std::discrete_distribution distribution(weights.begin(), weights.end()); return candidates[distribution(fallback_rng)]; } template double elapsed_ms(Fn && fn) { const auto start = std::chrono::steady_clock::now(); fn(); const auto end = std::chrono::steady_clock::now(); return std::chrono::duration(end - start).count(); } void test_matches_reference_fallback_sampler() { HfSampler sampler; HfSamplerScratch scratch; HfSamplingOptions options; options.do_sample = true; options.temperature = 0.82F; options.top_k = 64; options.top_p = 0.86F; options.min_tokens_to_keep = 2; options.repetition_penalty = 1.15F; for (int step = 0; step < 24; ++step) { const auto logits = make_logits(4096, step); const auto history = make_history(logits.size(), step); std::mt19937 reference_rng(1234 + step); std::mt19937 optimized_rng(1234 + step); const int32_t expected = reference_sample(logits, history, options, reference_rng, nullptr); const int32_t actual = sampler.sample(logits, history, options, scratch, optimized_rng, nullptr, "fallback"); engine::test::require_eq(actual, expected, "fallback sampled token"); } } void test_matches_reference_torch_sampler() { TorchCudaSamplingPolicy policy; policy.cuda_fast_path = true; policy.multiprocessor_count = 128; policy.max_threads_per_multiprocessor = 1536; HfTorchSamplingState torch_state; torch_state.policy = &policy; torch_state.seed = 5678; HfSampler sampler; HfSamplerScratch scratch; HfSamplingOptions options; options.do_sample = true; options.temperature = 0.75F; options.top_k = 0; options.top_p = 0.9F; options.min_tokens_to_keep = 1; options.repetition_penalty = 1.05F; for (int step = 0; step < 24; ++step) { torch_state.call_index = static_cast(step); const auto logits = make_logits(4096, step); const auto history = make_history(logits.size(), step); std::mt19937 reference_rng(1); std::mt19937 optimized_rng(1); const int32_t expected = reference_sample(logits, history, options, reference_rng, &torch_state); const int32_t actual = sampler.sample(logits, history, options, scratch, optimized_rng, &torch_state, "torch"); engine::test::require_eq(actual, expected, "torch sampled token"); } } void test_cuda_population_offset_matches_full_scores() { constexpr size_t population_size = 256; constexpr size_t token_offset = 91; constexpr size_t compact_size = 73; const float suppressed = -std::numeric_limits::infinity(); TorchCudaSamplingPolicy policy; policy.cuda_fast_path = true; policy.multiprocessor_count = 128; policy.max_threads_per_multiprocessor = 1536; HfSamplingOptions options; options.do_sample = true; options.temperature = 0.78F; options.top_p = 0.86F; options.repetition_penalty = 1.12F; HfSampler sampler; HfSamplerScratch full_scratch; HfSamplerScratch compact_scratch; std::mt19937 full_rng(1); std::mt19937 compact_rng(1); std::vector full_history{103, 127, 127, 151}; std::vector compact_history; for (const int32_t token : full_history) { compact_history.push_back(token - static_cast(token_offset)); } for (uint64_t step = 0; step < 16; ++step) { const auto compact_logits = make_logits(compact_size, static_cast(step)); std::vector full_logits(population_size, suppressed); std::copy( compact_logits.begin(), compact_logits.end(), full_logits.begin() + static_cast(token_offset)); HfTorchSamplingState full_state; full_state.policy = &policy; full_state.seed = 2468; full_state.call_index = step; HfTorchSamplingState compact_state = full_state; compact_state.population_size = population_size; compact_state.token_index_offset = token_offset; const int32_t full_token = sampler.sample( full_logits, full_history, options, full_scratch, full_rng, &full_state, "full population"); const int32_t compact_token = sampler.sample( compact_logits, compact_history, options, compact_scratch, compact_rng, &compact_state, "compact population"); engine::test::require_eq( compact_token + static_cast(token_offset), full_token, "CUDA population offset token"); } } void test_matches_python_hf_processor_reference_values() { std::vector scores{0.2F, -0.3F, 1.4F, 0.8F, 0.1F, 1.2F, -0.6F}; HfSamplerScratch scratch; engine::sampling::HfLogitsProcessor::apply_repetition_penalty( scores, std::vector{2, 2, 4}, 1.2F, scratch); engine::sampling::HfLogitsProcessor::apply_temperature(scores, 0.5F); engine::sampling::HfLogitsProcessor::apply_top_k(scores, 3, 1, scratch); engine::sampling::HfLogitsProcessor::apply_top_p(scores, 0.75F, 1, scratch); const std::vector expected{ -std::numeric_limits::infinity(), -std::numeric_limits::infinity(), 2.3333332538604736F, -std::numeric_limits::infinity(), -std::numeric_limits::infinity(), 2.4000000953674316F, -std::numeric_limits::infinity(), }; for (size_t index = 0; index < scores.size(); ++index) { if (std::isfinite(expected[index])) { engine::test::require_close(scores[index], expected[index], 1.0e-6F, "python HF processor score"); } else { engine::test::require(std::isinf(scores[index]) && scores[index] < 0.0F, "python HF processor mask"); } } } void test_min_p_masks_relative_probability_tail() { std::vector scores{4.0F, 2.0F, 1.0F, -1.0F}; HfSamplerScratch scratch; engine::sampling::HfLogitsProcessor::apply_min_p(scores, 0.1F, 1, scratch); engine::test::require(std::isfinite(scores[0]), "min-p keeps maximum token"); engine::test::require(std::isfinite(scores[1]), "min-p keeps token above relative threshold"); engine::test::require( std::isinf(scores[2]) && scores[2] < 0.0F, "min-p masks token below relative threshold"); engine::test::require( std::isinf(scores[3]) && scores[3] < 0.0F, "min-p masks probability tail"); std::vector protected_scores{4.0F, 0.0F, -1.0F}; engine::sampling::HfLogitsProcessor::apply_min_p( protected_scores, 0.9F, 2, scratch); engine::test::require( std::isfinite(protected_scores[0]) && std::isfinite(protected_scores[1]), "min-p honors min_tokens_to_keep"); } void test_matches_python_hf_cuda_multinomial_reference_sequence() { TorchCudaSamplingPolicy policy; policy.cuda_fast_path = true; policy.multiprocessor_count = 128; policy.max_threads_per_multiprocessor = 1536; HfTorchSamplingState torch_state; torch_state.policy = &policy; torch_state.seed = 1; torch_state.call_index = 0; HfSampler sampler; HfSamplerScratch scratch; HfSamplingOptions options; options.do_sample = true; options.temperature = 0.82F; options.top_k = 64; options.top_p = 0.86F; options.min_tokens_to_keep = 2; options.repetition_penalty = 1.15F; std::mt19937 fallback_rng(1); const std::vector expected{2537, 2526, 2513, 3960, 3934, 2507, 3946, 3933, 3921, 2478}; for (size_t step = 0; step < expected.size(); ++step) { torch_state.call_index = static_cast(step); const int generator_step = static_cast(3 + step); const int32_t actual = sampler.sample( make_logits(4096, generator_step), make_history(4096, generator_step), options, scratch, fallback_rng, &torch_state, "python HF cuda"); engine::test::require_eq(actual, expected[step], "python HF CUDA multinomial token sequence"); } } void test_matches_python_hf_cpu_multinomial_reference_sequence() { HfTorchSamplingState torch_state; torch_state.seed = 1; HfSampler sampler; HfSamplerScratch scratch; HfSamplingOptions options; options.do_sample = true; options.temperature = 0.82F; options.top_k = 64; options.top_p = 0.86F; options.min_tokens_to_keep = 2; options.repetition_penalty = 1.15F; std::mt19937 fallback_rng(1); const std::vector expected{3974, 2514, 2514, 3946, 2491, 2490, 3922, 2477, 2487, 2479}; for (size_t step = 0; step < expected.size(); ++step) { torch_state.call_index = static_cast(step); const int generator_step = static_cast(3 + step); const int32_t actual = sampler.sample( make_logits(4096, generator_step), make_history(4096, generator_step), options, scratch, fallback_rng, &torch_state, "python HF cpu"); engine::test::require_eq(actual, expected[step], "python HF CPU multinomial token sequence"); } } void test_greedy_fast_path_matches_reference() { HfSampler sampler; HfSamplerScratch scratch; HfSamplingOptions options; options.do_sample = false; options.repetition_penalty = 1.0F; std::mt19937 fallback_rng(1234); const auto logits = make_logits(8192, 7); const std::vector history; const int32_t expected = reference_sample(logits, history, options, fallback_rng, nullptr); const int32_t actual = sampler.sample(logits, history, options, scratch, fallback_rng, nullptr, "greedy"); engine::test::require_eq(actual, expected, "greedy token"); } void test_no_processor_sampling_fast_path_matches_reference() { HfSampler sampler; HfSamplerScratch scratch; HfSamplingOptions options; options.do_sample = true; options.temperature = 1.0F; options.top_k = 0; options.top_p = 1.0F; options.repetition_penalty = 1.0F; const auto logits = make_logits(8192, 9); const std::vector history; std::mt19937 reference_rng(4321); std::mt19937 optimized_rng(4321); const int32_t expected = reference_sample(logits, history, options, reference_rng, nullptr); const int32_t actual = sampler.sample(logits, history, options, scratch, optimized_rng, nullptr, "plain sample"); engine::test::require_eq(actual, expected, "plain sampled token"); } void test_no_processor_torch_fast_path_ignores_previous_candidates() { TorchCudaSamplingPolicy policy; policy.cuda_fast_path = true; policy.multiprocessor_count = 128; policy.max_threads_per_multiprocessor = 1536; HfTorchSamplingState torch_state; torch_state.policy = &policy; torch_state.seed = 77; HfSampler sampler; HfSamplerScratch scratch; HfSamplingOptions filtered_options; filtered_options.do_sample = true; filtered_options.temperature = 0.8F; filtered_options.top_k = 8; filtered_options.top_p = 0.75F; filtered_options.repetition_penalty = 1.1F; const auto first_logits = make_logits(2048, 3); const auto first_history = make_history(first_logits.size(), 3); std::mt19937 filtered_rng(1); torch_state.call_index = 0; (void)sampler.sample(first_logits, first_history, filtered_options, scratch, filtered_rng, &torch_state, "filtered"); HfSamplingOptions plain_options; plain_options.do_sample = true; plain_options.temperature = 1.0F; plain_options.top_k = 0; plain_options.top_p = 1.0F; plain_options.repetition_penalty = 1.0F; const auto plain_logits = make_logits(2048, 4); std::mt19937 reference_rng(2); std::mt19937 optimized_rng(2); torch_state.call_index = 1; const int32_t expected = reference_sample(plain_logits, {}, plain_options, reference_rng, &torch_state); const int32_t actual = sampler.sample(plain_logits, {}, plain_options, scratch, optimized_rng, &torch_state, "plain torch"); engine::test::require_eq(actual, expected, "plain torch stale candidates"); } void test_direct_torch_sampler_ignores_previous_score_buffer() { TorchCudaSamplingPolicy policy; policy.cuda_fast_path = true; policy.multiprocessor_count = 128; policy.max_threads_per_multiprocessor = 1536; HfTorchSamplingState torch_state; torch_state.policy = &policy; torch_state.seed = 88; HfSamplerScratch scratch; std::mt19937 fallback_rng(1); const auto first_scores = make_logits(1024, 1); torch_state.call_index = 0; (void)engine::sampling::HfTokenSampler::sample_from_processed_scores( first_scores, scratch, fallback_rng, &torch_state, "first"); const auto second_scores = make_logits(1024, 2); std::mt19937 reference_rng(2); std::mt19937 optimized_rng(2); HfSamplingOptions options; options.do_sample = true; torch_state.call_index = 1; const int32_t expected = reference_sample(second_scores, {}, options, reference_rng, &torch_state); const int32_t actual = engine::sampling::HfTokenSampler::sample_from_processed_scores( second_scores, scratch, optimized_rng, &torch_state, "second"); engine::test::require_eq(actual, expected, "direct torch stale score buffer"); } void benchmark_sampler_path() { constexpr int steps = 80; constexpr size_t vocab_size = 32768; const std::vector logits = make_logits(vocab_size, 0); const std::vector history = make_history(vocab_size, 0); TorchCudaSamplingPolicy policy; policy.cuda_fast_path = true; policy.multiprocessor_count = 128; policy.max_threads_per_multiprocessor = 1536; HfTorchSamplingState torch_state; torch_state.policy = &policy; torch_state.seed = 4321; HfSamplingOptions options; options.do_sample = true; options.temperature = 0.8F; options.top_k = 0; options.top_p = 0.9F; options.min_tokens_to_keep = 1; options.repetition_penalty = 1.1F; std::mt19937 reference_rng(1); const double reference_ms = elapsed_ms([&] { for (int step = 0; step < steps; ++step) { torch_state.call_index = static_cast(step); (void)reference_sample(logits, history, options, reference_rng, &torch_state); } }); HfSampler sampler; HfSamplerScratch scratch; std::mt19937 optimized_rng(1); const double optimized_ms = elapsed_ms([&] { for (int step = 0; step < steps; ++step) { torch_state.call_index = static_cast(step); (void)sampler.sample(logits, history, options, scratch, optimized_rng, &torch_state, "bench"); } }); std::cout << "hf_sampler_reference_ms " << reference_ms << '\n'; std::cout << "hf_sampler_optimized_ms " << optimized_ms << '\n'; std::cout << "hf_sampler_speedup " << (reference_ms / optimized_ms) << '\n'; } } // namespace int main() { try { test_greedy_fast_path_matches_reference(); test_matches_python_hf_processor_reference_values(); test_min_p_masks_relative_probability_tail(); test_matches_python_hf_cuda_multinomial_reference_sequence(); test_matches_python_hf_cpu_multinomial_reference_sequence(); test_no_processor_sampling_fast_path_matches_reference(); test_no_processor_torch_fast_path_ignores_previous_candidates(); test_direct_torch_sampler_ignores_previous_score_buffer(); test_matches_reference_fallback_sampler(); test_matches_reference_torch_sampler(); test_cuda_population_offset_matches_full_scores(); benchmark_sampler_path(); } catch (const std::exception & error) { std::cerr << error.what() << '\n'; return 1; } std::cout << "hf_sampler_test passed\n"; return 0; }