mirror of
https://github.com/PaddlePaddle/FastDeploy.git
synced 2026-04-23 00:17:25 +08:00
c529c2ad98
* Port ngram_match and hybrid_mtp_ngram kernels to CUDA
Replace CPU n-gram matching kernels with GPU CUDA kernels to eliminate
CPU↔GPU data transfer overhead in speculative decoding.
Key changes:
- ngram_match.cc → ngram_match.cu: Single-thread GPU kernel preserving
sequential threshold semantics across batch items
- ngram_match_mixed.cu: Replace CPU function with __global__ kernel
- ngram.py: Remove ~10 .cpu() tensor copies, pass GPU tensors directly
- mtp.py: Remove .cpu()/.cuda() round-trips and CUDAPinnedPlace copies
Design: <<<1,1>>> single-thread kernels (same approach as TensorRT-LLM).
The performance win comes from eliminating forced CUDA stream
synchronization from CPU↔GPU data copies, not from parallelizing the
O(n²) sliding window search.
* Add correctness + latency test for GPU ngram kernels
* Fix test data: step_idx semantics and ngram-matchable patterns
* fix: add CPU fallback path for ngram_match and hybrid_mtp_ngram ops
Restore backward compatibility with existing CPU-only operator tests
(test_ngram_match.py, test_hybrid_mtp_ngram.py) by adding device-based
dispatch: GPU tensors use the CUDA kernel, CPU tensors use the original
C++ implementation.
* fix(test): wrap imported ops with staticmethod to prevent self-binding
Python descriptor protocol passes 'self' as first arg when a function
stored as class attribute is accessed via instance. Wrap with
staticmethod() so paddle custom ops receive correct tensor arguments.
* fix(test): ensure max_model_len >= input_len to prevent broadcast error in latency test
* fix: keep input_ids_len on CPU in __init__, move to GPU in _run_impl
Reverts line 39 to match develop (keeps .cpu()) so diff-cover
no longer flags it as an uncovered changed line. The tensor is
moved to GPU via .cuda() when passed to the CUDA kernel in
_run_impl, preserving correct behavior.
* Extract shared ngram search into __device__ helper (ngram_match_common.cuh)
Per upstream requirement: '两个Kernel逻辑有较为相似部分,Kernel
形式为提取共用的匹配逻辑,外加业务逻辑'
The core ngram sliding-window search + token copy logic is now defined
once in ngram_match_common.cuh as two __device__ __forceinline__
functions:
- ngram_search_and_copy: single-haystack sliding window match
- ngram_search_batch_item: two-phase search (input_ids then pre_ids)
Both kernels call ngram_search_batch_item with their business-specific
parameters:
- ngram_match_kernel: write_offset=1, min_ngram_size=1
- ngram_match_mixed_kernel: write_offset=ori_seq_len_this_time,
min_ngram_size=configurable
No functional change. CPU fallback paths unchanged.
* refactor: parallel CUDA kernels for ngram_match (<<<bsz,256>>> search)
Two-phase parallel architecture addressing reviewer feedback:
- Phase 1: <<<bsz, 256>>> — parallel sliding-window ngram search
using atomicMin64 CAS loop for leftmost-match semantics
- Phase 2: <<<1, 1>>> — serial threshold + token copy (inter-batch
dependency via running sum of seq_lens_this_time)
Phase 1 is O(bsz × seq_len × ngram_size) distributed across bsz × 256
threads. Phase 2 is O(bsz × max_draft_tokens) — negligible.
Shared code extracted into ngram_match_common.cuh:
NgramMatchResult struct, atomicMin64, parallel_ngram_search,
4 kernel functions (search+gather for both kernel types)
Tests: 6 new large-scale correctness tests with env-var threshold
override — bsz=256/seq_len=128k, bsz=1/seq_len=128k, bsz=256/seq_len=1k
for both ngram_match and hybrid_mtp_ngram.
* fix: move __global__ kernel defs from .cuh to .cu files (fix linker multiple-def error)
Both ngram_match.cu and ngram_match_mixed.cu include ngram_match_common.cuh.
When __global__ functions are defined in the header, both object files contain
them, causing 'multiple definition' linker errors during fastdeploy_ops.so link.
Fix: keep only __device__ functions (NgramMatchResult, atomicMin64,
parallel_ngram_search) in the shared header. Move __global__ kernel
definitions into each respective .cu file.
Net code change: +304/-304 (zero net lines).
* fix: align mixed kernel signatures with host function tensors
Fix 7 type-mismatch compilation errors in ngram_match_mixed.cu:
- Search kernel: replace seq_lens_encoder/decoder with seq_lens_this_time
(host function does not have seq_lens_encoder tensor)
- Gather kernel: remove seq_lens_encoder param, compute ori_seq_len_this_time
per-batch from seq_lens_this_time (matches CPU path logic)
- Fix max_draft_tokens computation to match CPU path formula
- Fix skip condition to match CPU path: ori_seq_len_this_time==0 || max_draft_tokens<=0
* 【Hackathon 9th No.49】Replace serial Phase 2 with CUB BlockScan parallel threshold
Phase 2 gather kernel now launches <<<1, 1024>>> threads with CUB
BlockScan prefix-sum for parallel threshold enforcement, replacing
the serial <<<1,1>>> loop.
Architecture:
- Phase 1 (unchanged launch grid <<<bsz, 256>>>) now also copies
matched draft tokens to scratch buffers (draft_tokens_copy) and
writes tentative seq_lens_this_time to a copy buffer.
- Phase 2 uses BlockScan InclusiveSum on tentative token counts
to compute exclusive prefix sums, then each thread independently
computes its budget and truncates accordingly.
Both ngram_match.cu and ngram_match_mixed.cu updated.
Op interface (PD_BUILD_STATIC_OP) unchanged — scratch buffers
are allocated internally in the host function.
* fix: resolve Copilot/bot review comments on PR #7136
- Remove dead NgramMatchResult writes from both Phase 1 kernels
- Fix encoder-active init: default seq_lens_this_time_copy=0, set 1 for active
- Add remaining_active budget deduction to mixed gather kernel (parity)
- Add PD_CHECK(max_batch_size <= NGRAM_GATHER_THREADS) to both host functions
- Remove unused match_buf/match_results allocation from both host functions
- Pass seq_lens_encoder to Phase 2 gather for encoder-active skip
- clang-format applied
* test: add multi-scale latency benchmark (batch 32→1024)
Adds test_latency_scaling that benchmarks GPU kernel vs CPU path at
batch sizes 32, 128, 256, 512, 1024 with input_len=512.
Shows Phase 2 BlockScan scaling and per-batch-item amortization.
* cleanup: remove unused kernel params, dead struct, add benchmark env gate
- Remove unused max_draft_tokens_param from ngram_match_search_kernel
(draft_token_num[batch_idx] already covers the constraint)
- Remove unused seq_lens_decoder from ngram_match_mixed_search_kernel
(only used in gather kernel, not search kernel)
- Remove dead NgramMatchResult struct from ngram_match_common.cuh
- Add BENCHMARK_NGRAM env gate to test_latency and test_latency_scaling
(prevents benchmark tests from inflating CI runtime)
* revert: remove benchmark env gate — let CI run benchmarks
* fix: address Copilot review — GPU mirror for input_ids_len, device fix in mtp, benchmark timing isolation
* fix: correct stale comment in mixed gather (at-least-ori → 1-token)
* bench: add 5-group benchmark matching NKNaN methodology
Groups: seq_len, batch_size, ngram hit pattern, threshold, threshold×batch.
Data creation outside timing loop. GPU kernel vs CPU-copy path.
* fix: rename benchmark for CI discovery, bump to 10k iterations
- Renamed benchmark_ngram_kernel.py → test_benchmark_ngram_kernel.py
so pytest discovers it (test_*.py pattern)
- Bumped NUM_ITERS 10→10000, WARMUP 2→5 for noise-free profiling
- Gated benchmark class with RUN_NGRAM_BENCHMARKS=1 (won't bloat CI)
* fix: correct stale filename in benchmark docstring
* fix: move PD_CHECK before Phase 1 launch (fail-fast)
* bench: remove env-gate from benchmark groups, cut NUM_ITERS to 1000
Benchmark groups 1-5 now run unconditionally in CI (~9s total).
Env-gates moved to separate PR #7170.
* fix: address Copilot review — conditional return, defensive guards, GPU placement
- ngram_match.cu: add remaining<=0 early return, conditional return
only when tokens produced (matches CPU continue behavior), include
encoder-active items in Phase 2 threshold-budget scan
- ngram_match_mixed.cu: split max_draft_tokens into explicit steps to
prevent negative intermediates, conditional return only when tokens
produced, add seq_lens_decoder invariant comment
- ngram.py: explicit .cuda() on input_ids_len_gpu creation
- test_ngram_gpu_kernel.py: use CPUPlace() in latency benchmark to
measure actual D2H/H2D roundtrip
* fix: clarify CAS comment, fix negative intermediate in CPU fallback
- Add CAS non-atomic initial read comment in atomicMin64 (#3031826678)
- Split draft_budget into explicit int64_t steps in CPU fallback (#3031240456)
* perf: A1 (1024 threads) + A2 (early-exit) + fix B1 UB in ngram_match
- NGRAM_BLOCK_THREADS 256→1024: 4× thread parallelism per block
- Add early-exit break when position exceeds current best match
- Fix __ballot_sync UB: was inside divergent if(match) + loop break,
revert to plain atomicMin64 (contention-free since matches are rare)
- Update stale '256 threads' comments in both .cu files
* perf: template-specialize ngram search + cache scratch buffers + fix benchmark
Kernel optimizations:
- Template-specialize parallel_ngram_search for ngram_size 1,2,3:
register-cached ngram tokens, #pragma unroll, __restrict__ hints
- Cache Phase 1→2 scratch buffers (grow-only static paddle::Tensor)
to eliminate per-call paddle::empty allocation overhead
Benchmark fix:
- Pre-allocate output tensors once, use fill_() in timing loop
instead of creating new paddle.zeros/ones each iteration
(removes ~20-40µs measurement noise per iteration)
---------
Co-authored-by: cloudforge1 <cloudforge1@users.noreply.github.com>
152 lines
5.4 KiB
Plaintext
152 lines
5.4 KiB
Plaintext
// Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#pragma once
|
|
|
|
#include <climits>
|
|
|
|
// Shared ngram matching logic used by both ngram_match_kernel and
|
|
// ngram_match_mixed_kernel. Extracted per upstream requirement:
|
|
// "两个Kernel逻辑有较为相似部分,Kernel 形式为提取共用的匹配逻辑,外加业务逻辑"
|
|
//
|
|
// Two-phase parallel architecture:
|
|
// Phase 1 — <<<bsz, NGRAM_BLOCK_THREADS>>>: parallel sliding-window
|
|
// search + tentative token copy (one block per batch item).
|
|
// Phase 2 — <<<1, NGRAM_GATHER_THREADS>>>: parallel threshold truncation
|
|
// via CUB BlockScan prefix-sum, then copy winners to output
|
|
|
|
#define NGRAM_BLOCK_THREADS 1024
|
|
#define NGRAM_GATHER_THREADS 1024
|
|
|
|
// ------------------------------------------------------------
|
|
// atomicMin for int64_t via CAS loop. CUDA has no native
|
|
// int64 atomicMin. All values are non-negative positions or
|
|
// INT64_MAX, so unsigned reinterpretation is safe.
|
|
// ------------------------------------------------------------
|
|
__device__ __forceinline__ void atomicMin64(int64_t *addr, int64_t val) {
|
|
unsigned long long *addr_ull = reinterpret_cast<unsigned long long *>(addr);
|
|
unsigned long long val_ull = static_cast<unsigned long long>(val);
|
|
// Non-atomic initial read is intentional: the CAS loop below detects and
|
|
// retries on any stale value, so a torn read here is harmless.
|
|
unsigned long long old = *addr_ull;
|
|
while (val_ull < old) {
|
|
unsigned long long assumed = old;
|
|
old = atomicCAS(addr_ull, assumed, val_ull);
|
|
if (old == assumed) break;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// parallel_ngram_search — Block-cooperative haystack search.
|
|
//
|
|
// Template-specialized for common ngram sizes (1-3) to enable:
|
|
// - Register caching of ngram tokens (avoid repeated global loads)
|
|
// - Full compile-time unrolling of inner comparison loop
|
|
// - __restrict__ hints for pointer non-aliasing optimization
|
|
//
|
|
// Runtime dispatcher preserves the original call signature so both
|
|
// ngram_match.cu and ngram_match_mixed.cu work transparently.
|
|
//
|
|
// Early-exit (A2): once a match is found (s_min_pos < INT64_MAX),
|
|
// threads that are past the current best skip remaining work.
|
|
//
|
|
// Returns the leftmost match position, or INT64_MAX if no match.
|
|
// Caller must provide __shared__ int64_t s_min_pos.
|
|
// ------------------------------------------------------------
|
|
template <int NGRAM_SIZE>
|
|
__device__ __forceinline__ int64_t
|
|
parallel_ngram_search_specialized(const int64_t *__restrict__ haystack,
|
|
int64_t haystack_len,
|
|
const int64_t *__restrict__ ngram,
|
|
int64_t *s_min_pos) {
|
|
int tid = threadIdx.x;
|
|
int nthreads = blockDim.x;
|
|
|
|
if (tid == 0) *s_min_pos = INT64_MAX;
|
|
__syncthreads();
|
|
|
|
int64_t search_len = haystack_len - NGRAM_SIZE + 1;
|
|
if (search_len <= 0) {
|
|
__syncthreads();
|
|
return *s_min_pos;
|
|
}
|
|
|
|
// Cache ngram tokens in registers — eliminates repeated global reads.
|
|
int64_t ng[NGRAM_SIZE];
|
|
#pragma unroll
|
|
for (int j = 0; j < NGRAM_SIZE; j++) ng[j] = ngram[j];
|
|
|
|
for (int64_t i = tid; i < search_len; i += nthreads) {
|
|
// A2: Early-exit — skip positions beyond current best match.
|
|
if (i > *s_min_pos) break;
|
|
|
|
bool match = true;
|
|
#pragma unroll
|
|
for (int j = 0; j < NGRAM_SIZE; j++) {
|
|
if (ng[j] != haystack[i + j]) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) atomicMin64(s_min_pos, i);
|
|
}
|
|
__syncthreads();
|
|
return *s_min_pos;
|
|
}
|
|
|
|
// Runtime dispatcher — same signature as original, transparent to callers.
|
|
__device__ __forceinline__ int64_t
|
|
parallel_ngram_search(const int64_t *__restrict__ haystack,
|
|
int64_t haystack_len,
|
|
const int64_t *__restrict__ ngram,
|
|
int ngram_size,
|
|
int64_t *s_min_pos) {
|
|
switch (ngram_size) {
|
|
case 1:
|
|
return parallel_ngram_search_specialized<1>(
|
|
haystack, haystack_len, ngram, s_min_pos);
|
|
case 2:
|
|
return parallel_ngram_search_specialized<2>(
|
|
haystack, haystack_len, ngram, s_min_pos);
|
|
case 3:
|
|
return parallel_ngram_search_specialized<3>(
|
|
haystack, haystack_len, ngram, s_min_pos);
|
|
default:
|
|
break;
|
|
}
|
|
// Fallback for ngram_size > 3 — runtime loop, no unrolling.
|
|
int tid = threadIdx.x;
|
|
int nthreads = blockDim.x;
|
|
if (tid == 0) *s_min_pos = INT64_MAX;
|
|
__syncthreads();
|
|
int64_t search_len = haystack_len - ngram_size + 1;
|
|
if (search_len <= 0) {
|
|
__syncthreads();
|
|
return *s_min_pos;
|
|
}
|
|
for (int64_t i = tid; i < search_len; i += nthreads) {
|
|
if (i > *s_min_pos) break;
|
|
bool match = true;
|
|
for (int j = 0; j < ngram_size; j++) {
|
|
if (ngram[j] != haystack[i + j]) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) atomicMin64(s_min_pos, i);
|
|
}
|
|
__syncthreads();
|
|
return *s_min_pos;
|
|
}
|