Skip to content

Commit 94b281b

Browse files
authored
[https://nvbugs/6242591][fix] Fix bugs in Beam Search kernels (NVIDIA#15621)
Signed-off-by: wili <12345678+wili@users.noreply.github.com>
1 parent 4d1cb8f commit 94b281b

5 files changed

Lines changed: 158 additions & 42 deletions

File tree

cpp/tensorrt_llm/kernels/beamSearchKernels.cu

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include "tensorrt_llm/common/config.h"
1818
#include "tensorrt_llm/common/cudaUtils.h"
19+
#include "tensorrt_llm/common/reduceKernelUtils.cuh"
1920
#include "tensorrt_llm/kernels/beamSearchKernels.h"
2021

2122
using namespace tensorrt_llm::common;
@@ -135,21 +136,26 @@ void invokeUpdateCacheIndirection(int* tgtCI, int const* srcCI, BeamHypotheses&
135136
sync_check_cuda_error(stream);
136137
}
137138

138-
__global__ void addCumLogProbs(float* __restrict pStage1LogProbs, float const* __restrict cumLogProbs,
139-
FinishedState const* finished, int const* endIds, float const* diversityRates,
139+
__global__ void addCumLogProbs(float* __restrict pStage1LogProbs, int const* __restrict pStage1Ids,
140+
float const* __restrict cumLogProbs, FinishedState const* finished, int const* endIds, float const* diversityRates,
140141
runtime::SizeType32 const* batchSlots, size_t const nBS, size_t const nBMIn, size_t const nBMOut, size_t const nBM)
141142
{
142143
int const bid = blockIdx.x; // Index of request in batch
143144
runtime::SizeType32 const slot = batchSlots[bid];
144145
float const diversityRate{diversityRates[slot]};
145146
float* pLocalLogProbs = pStage1LogProbs + bid * nBMIn * nBMOut * 2;
147+
int const* pLocalIds = pStage1Ids + bid * nBMIn * nBMOut * 2;
146148

147149
for (int i = threadIdx.x; i < nBMIn * nBMOut * 2; i += blockDim.x)
148150
{
149151
int const iBMIn = i / (nBMOut * 2);
150-
if (finished[slot * nBMIn + iBMIn].isFinished())
152+
if (finished[slot * nBM + iBMIn].isFinished())
151153
{
152-
pLocalLogProbs[i] += (i == endIds[slot]) ? 1.0f : 0.0f;
154+
// In V2 path, i is a candidate-slot index (0..nBMIn*nBMOut*2-1), NOT a vocab token id.
155+
// Use pStage1Ids to look up the actual token id for the EOS comparison.
156+
bool const isEOS = (pLocalIds[i] == endIds[slot]);
157+
// Keep only the EOS candidate with its proper cumulative score; suppress all others.
158+
pLocalLogProbs[i] = isEOS ? (pLocalLogProbs[i] + cumLogProbs[slot * nBM + iBMIn]) : -FLT_MAX;
153159
}
154160
else
155161
{
@@ -160,21 +166,27 @@ __global__ void addCumLogProbs(float* __restrict pStage1LogProbs, float const* _
160166
return;
161167
}
162168

163-
__global__ void addCumLogProbs(half* __restrict pStage1LogProbs, float const* __restrict cumLogProbs,
164-
FinishedState const* finished, int const* endIds, float const* diversityRates,
169+
__global__ void addCumLogProbs(half* __restrict pStage1LogProbs, int const* __restrict pStage1Ids,
170+
float const* __restrict cumLogProbs, FinishedState const* finished, int const* endIds, float const* diversityRates,
165171
runtime::SizeType32 const* batchSlots, size_t const nBS, size_t const nBMIn, size_t const nBMOut, size_t const nBM)
166172
{
167173
int const bid = blockIdx.x; // Index of request in batch
168174
runtime::SizeType32 const slot = batchSlots[bid];
169175
float const diversityRate{diversityRates[slot]};
170176
half* pLocalLogProbs = pStage1LogProbs + bid * nBMIn * nBMOut * 2;
177+
int const* pLocalIds = pStage1Ids + bid * nBMIn * nBMOut * 2;
171178

172179
for (int i = threadIdx.x; i < nBMIn * nBMOut * 2; i += blockDim.x)
173180
{
174181
int const iBMIn = i / (nBMOut * 2);
175-
if (finished[slot * nBMIn + iBMIn].isFinished())
182+
if (finished[slot * nBM + iBMIn].isFinished())
176183
{
177-
pLocalLogProbs[i] += (i == endIds[slot]) ? 1.0f : 0.0f;
184+
// In V2 path, i is a candidate-slot index (0..nBMIn*nBMOut*2-1), NOT a vocab token id.
185+
// Use pStage1Ids to look up the actual token id for the EOS comparison.
186+
bool const isEOS = (pLocalIds[i] == endIds[slot]);
187+
// Keep only the EOS candidate with its proper cumulative score; suppress all others.
188+
pLocalLogProbs[i]
189+
= isEOS ? (half) (float(pLocalLogProbs[i]) + cumLogProbs[slot * nBM + iBMIn]) : (half) -HALF_FLT_MAX;
178190
}
179191
else
180192
{

cpp/tensorrt_llm/kernels/beamSearchKernels.h

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,15 @@ void invokeTopkBeamSearch(T const* logProbs, T const* bias, void* workspace, Bea
131131
void invokeUpdateCacheIndirection(int* tgtCI, int const* srcCI, BeamHypotheses& bh,
132132
runtime::SizeType32 const maxAttentionWindow, runtime::SizeType32 sinkTokenLength, cudaStream_t stream);
133133

134-
__global__ void addCumLogProbs(float* __restrict pStage1LogProbs, float const* __restrict cumLogProbs,
135-
::tensorrt_llm::kernels::FinishedState const* finished, int const* endIds, float const* diversityRates,
136-
runtime::SizeType32 const* batchSlots, size_t const nBS, size_t const nBMIn, size_t const nBMOut, size_t const nBM);
137-
138-
__global__ void addCumLogProbs(half* __restrict pStage1LogProbs, float const* __restrict cumLogProbs,
139-
::tensorrt_llm::kernels::FinishedState const* finished, int const* endIds, float const* diversityRates,
140-
runtime::SizeType32 const* batchSlots, size_t const nBS, size_t const nBMIn, size_t const nBMOut, size_t const nBM);
134+
__global__ void addCumLogProbs(float* __restrict pStage1LogProbs, int const* __restrict pStage1Ids,
135+
float const* __restrict cumLogProbs, ::tensorrt_llm::kernels::FinishedState const* finished, int const* endIds,
136+
float const* diversityRates, runtime::SizeType32 const* batchSlots, size_t const nBS, size_t const nBMIn,
137+
size_t const nBMOut, size_t const nBM);
138+
139+
__global__ void addCumLogProbs(half* __restrict pStage1LogProbs, int const* __restrict pStage1Ids,
140+
float const* __restrict cumLogProbs, ::tensorrt_llm::kernels::FinishedState const* finished, int const* endIds,
141+
float const* diversityRates, runtime::SizeType32 const* batchSlots, size_t const nBS, size_t const nBMIn,
142+
size_t const nBMOut, size_t const nBM);
141143

142144
__global__ void gatherId(int const* __restrict pStage1Id, int* __restrict pStage2Id, size_t const nBS,
143145
size_t const nBMIn, size_t const nBMOut, size_t const nV);

cpp/tensorrt_llm/kernels/beamSearchKernels/beamSearchKernelsTemplate.h

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ __launch_bounds__(BLOCK_SIZE) __global__ void beamStage3Kernel(
208208
__shared__ float smemCumLogProbs[PBM];
209209
__shared__ int smemSeqLen[PBM];
210210
__shared__ KVPair smemTopKV[(IS_V2) ? 1 : PBM * 2]; // Just a placeholder in V2 workflow
211+
__shared__ int smemNBeamForNextStep;
211212

212213
if (bh.numBeamsCBA != nullptr)
213214
{
@@ -217,14 +218,11 @@ __launch_bounds__(BLOCK_SIZE) __global__ void beamStage3Kernel(
217218
// Initialize worst score in the first call
218219
bh.minNormedScoresCBA[slot] = 0.0f; // logProbs is in range (-inf, 0]
219220
}
220-
else if (earlyStopping == 1 && bh.numBeamsCBA[slot] == nBM
221-
|| earlyStopping != 1 && bh.finished[slot * nBM].isFinished())
221+
else if (earlyStopping == 1 && bh.numBeamsCBA[slot] >= nBM || earlyStopping != 1 && bh.batchDones[slot])
222222
{
223223
// Condition of early return:
224224
// 1. In EarlyStopping mode, and we have got enough beams
225225
// 2. In NonEarlyStopping mode, and this batch has been marked as done
226-
// TODO: improve the condition like below
227-
// earlyStopping == 1 && bh.numBeamsCBA[slot] == nBM || earlyStopping != 1 && bh.batchDones[slot]
228226
return;
229227
}
230228
}
@@ -296,6 +294,11 @@ __launch_bounds__(BLOCK_SIZE) __global__ void beamStage3Kernel(
296294
}
297295
__syncthreads();
298296

297+
// Timestep at which each next-step destination beam's token is stored in the work tree.
298+
// This is the parent beam's sequence length (the true generation step), which may differ
299+
// from the destination slot's own (possibly stale) length when a finished slot is reused.
300+
__shared__ int smemWriteStep[PBM];
301+
299302
if (tid == 0)
300303
{
301304
int nBeamForNextStep{0};
@@ -324,10 +327,14 @@ __launch_bounds__(BLOCK_SIZE) __global__ void beamStage3Kernel(
324327
{
325328
// Condition of this branch:
326329
// This token is end-token and belongs to top nBM range in Beam search mode
327-
int const nSeqLen = bh.sequenceLengths[slot * nBM + i] + 1 - bh.inputLengths[slot * nBM + i];
330+
// Use the actual parent beam index (topId / nV) % nBM, not the candidate rank i,
331+
// to look up the correct sequenceLength and inputLength for length-penalty scoring.
332+
int const parentBeam = (topId / nV) % nBM;
333+
int const nSeqLen
334+
= bh.sequenceLengths[slot * nBM + parentBeam] + 1 - bh.inputLengths[slot * nBM + parentBeam];
328335
float const score = applyLengthPenalty(topLogProb, nSeqLen, lengthPenalty);
329336
int nCBA = bh.numBeamsCBA[slot];
330-
if (nCBA == nBM)
337+
if (nCBA >= nBM)
331338
{
332339
// There are already nBM beams
333340
if (score < bh.minNormedScoresCBA[slot])
@@ -407,13 +414,18 @@ __launch_bounds__(BLOCK_SIZE) __global__ void beamStage3Kernel(
407414
// 1. bh.numBeamsCBA == nullptr && i < nBM, i.e., beam search is disable
408415
// 2. bh.numBeamsCBA != nullptr && i < nBM && isEndToken == false, i.e., add token at the end
409416
// 3. bh.numBeamsCBA != nullptr && i >= nBM && isEndToken == false, i.e., add token at the end
410-
int const step = bh.sequenceLengths[slot * nBM + nBeamForNextStep];
417+
// Write at the parent beam's sequence length (the actual generation step),
418+
// not the destination slot's length, which can be stale if the slot was
419+
// previously finished and is now being reused for a new continuation.
420+
int const parentBeam = topId / nV % nBM;
421+
int const step = bh.sequenceLengths[slot * nBM + parentBeam];
422+
smemWriteStep[nBeamForNextStep] = step;
411423
// Copy the selected token to work tree
412424
bh.outputIdsPtr[slot][nBeamForNextStep * nMSL + step] = topId;
413425
if (bh.logProbsTiled != nullptr)
414426
{
415427
int const index = step * nMBS * nBM + slot * nBM + nBeamForNextStep;
416-
int const indexBeam = topId / nV % nBM;
428+
int const indexBeam = parentBeam;
417429
bh.logProbsTiled[index] = (float) topLogProb - smemCumLogProbs[indexBeam];
418430
}
419431
bh.cumLogProbs[slot * nBM + nBeamForNextStep] = (float) topLogProb;
@@ -437,6 +449,7 @@ __launch_bounds__(BLOCK_SIZE) __global__ void beamStage3Kernel(
437449
break;
438450
}
439451
}
452+
smemNBeamForNextStep = nBeamForNextStep;
440453
}
441454

442455
// Update bh.batchDones
@@ -481,23 +494,40 @@ __launch_bounds__(BLOCK_SIZE) __global__ void beamStage3Kernel(
481494
if (tid < nBMOut)
482495
{
483496
int const indexBatchBeam = slot * nBM + tid;
484-
int const step = smemSeqLen[tid];
485-
if (!bh.finished[indexBatchBeam].isFinished())
497+
if (tid < smemNBeamForNextStep)
486498
{
487-
smemSeqLen[tid]++;
499+
// This slot received a valid next-step token from the selection phase.
500+
// Use the timestep recorded by the selection phase (the parent beam's length),
501+
// which matches the position where the encoded token was stored.
502+
int const step = smemWriteStep[tid];
503+
int const newId = bh.outputIdsPtr[slot][tid * nMSL + step];
504+
int const newBeamId = (newId / nV) % nBM;
505+
int const newTokenId = newId % nV;
506+
int const indexParentBeam = slot * nBM + newBeamId;
507+
int const parentSeqLen = smemSeqLen[newBeamId];
508+
bh.sequenceLengths[indexBatchBeam] = parentSeqLen + (!bh.finished[indexParentBeam].isFinished() ? 1 : 0);
509+
if (newTokenId == bh.endIds[slot])
510+
{
511+
bh.finished[indexBatchBeam].setFinishedEOS();
512+
}
513+
else
514+
{
515+
// Reset any stale finished state: this slot may have been marked finished in a
516+
// previous step and is now reused for a valid non-EOS beam; otherwise it would be
517+
// wrongly skipped downstream.
518+
bh.finished[indexBatchBeam] = FinishedState::empty();
519+
}
520+
bh.parentIdsPtr[slot][tid * nMSL + step] = newBeamId;
521+
bh.outputIdsPtr[slot][tid * nMSL + step] = newTokenId;
488522
}
489-
int const newId = bh.outputIdsPtr[slot][tid * nMSL + step];
490-
int const newBeamId = (newId / nV) % nBM;
491-
int const newTokenId = newId % nV;
492-
bh.sequenceLengths[indexBatchBeam] = smemSeqLen[newBeamId];
493-
if (newTokenId == bh.endIds[slot])
523+
else
494524
{
495-
bh.finished[indexBatchBeam].setFinishedEOS();
525+
// No valid next-step token for this slot: all top candidates went to CBA.
526+
// Mark as finished so downstream stages (cache indirection, next decode) skip it.
527+
bh.finished[indexBatchBeam].setFinished();
496528
}
497-
bh.parentIdsPtr[slot][tid * nMSL + step] = newBeamId;
498-
bh.outputIdsPtr[slot][tid * nMSL + step] = newTokenId;
499529

500-
if ((earlyStopping == 1) && (bh.numBeamsCBA != nullptr && bh.numBeamsCBA[slot] == nBM)
530+
if ((earlyStopping == 1) && (bh.numBeamsCBA != nullptr && bh.numBeamsCBA[slot] >= nBM)
501531
|| (earlyStopping != 1) && bh.batchDones[slot])
502532
{
503533
bh.batchDones[slot] = true;
@@ -631,7 +661,7 @@ void beamSearchKernelLauncher(
631661
sync_check_cuda_error(stream);
632662

633663
int nThread = min(roundUp(nBMIn * nBMOut * 2, 32), 1024);
634-
addCumLogProbs<<<nBS, nThread, 0, stream>>>(pStage1LogProbs, bh.cumLogProbs, bh.finished, bh.endIds,
664+
addCumLogProbs<<<nBS, nThread, 0, stream>>>(pStage1LogProbs, pStage1Ids, bh.cumLogProbs, bh.finished, bh.endIds,
635665
bh.diversityRates, bh.batchSlots, nBS, nBMIn, nBMOut, nBM);
636666
sync_check_cuda_error(stream);
637667

cpp/tensorrt_llm/kernels/decodingKernels.cu

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ __global__ void gatherTree(gatherTreeParam param)
122122
{
123123
int const levelBeamIx = batch * param.beamWidth * param.maxSeqLen + beam * param.maxSeqLen + level;
124124
int const levelParentIx = batch * param.beamWidth * param.maxSeqLen + parent * param.maxSeqLen + level;
125-
if (parent < 0 || parent > param.beamWidth)
125+
if (parent < 0 || parent >= param.beamWidth)
126126
{
127127
param.outputIds[levelBeamIx] = param.endTokens[batch];
128128
parent = -1;
@@ -702,10 +702,11 @@ __global__ void transposeLogProbs(float* outputLogProbs, float* outputLogProbsTi
702702
}
703703

704704
auto const batchSlot = batchSlots[batchIdx];
705-
if (pos < sequenceLengths[batchSlot])
705+
auto const batchBeamIdx = batchSlot * beamWidth + beamIdx;
706+
if (pos < sequenceLengths[batchBeamIdx])
706707
{
707-
auto const batchBeamIdx = batchSlot * beamWidth * maxSeqLen + beamIdx * maxSeqLen + pos;
708-
outputLogProbs[batchBeamIdx]
708+
auto const outputIndex = batchSlot * beamWidth * maxSeqLen + beamIdx * maxSeqLen + pos;
709+
outputLogProbs[outputIndex]
709710
= outputLogProbsTiled[pos * maxBatchSize * beamWidth + batchSlot * beamWidth + beamIdx];
710711
}
711712
}

tests/unittest/_torch/sampler/test_beam_search.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323

2424
import pytest
2525
import torch
26-
from test_beam_search_util import (BeamSearchTestOutput, DummyConfigLoader,
27-
DummyWeightLoader, get_expected_outputs)
26+
from test_beam_search_util import (BeamSearchTestOutput, DummyConfig,
27+
DummyConfigLoader, DummyWeightLoader,
28+
get_expected_outputs)
2829
from utils.llm_data import llm_models_root
2930
from utils.util import assert_no_cuda_sync, force_ampere, run_test_with_warmup
3031

@@ -543,6 +544,76 @@ def test_beam_search_disagg_e2e(
543544
gen_llm.shutdown()
544545

545546

547+
@pytest.mark.parametrize("beam_width", [10])
548+
@pytest.mark.threadleak(enabled=False)
549+
def test_beam_search_large_beam_width_regression(
550+
beam_width: int,
551+
monkeypatch: pytest.MonkeyPatch,
552+
) -> None:
553+
"""
554+
Fix https://nvbugs/6242591
555+
"""
556+
num_prompts = 2
557+
input_prompts = [[1, 2, 3], [4, 5, 6]]
558+
vocab_size = DummyConfig().vocab_size
559+
# The dummy model pushes each beam towards (token + 3) mod vocab_size, so an
560+
# end id a few multiples of 3 ahead of the prompt is reachable and different
561+
# beams hit it at different steps -> exercises the finished-slot reuse path.
562+
end_id = 24
563+
564+
checkpoint_loader = HfCheckpointLoader(
565+
weight_loader=DummyWeightLoader(),
566+
config_loader=DummyConfigLoader(),
567+
)
568+
569+
gc.collect(2) # force destruction of any other LLM instances
570+
with _single_process_context():
571+
llm = LLM(
572+
model=_pl.Path("dummy_path"),
573+
checkpoint_loader=checkpoint_loader,
574+
sampler_type="TRTLLMSampler",
575+
max_beam_width=beam_width,
576+
max_batch_size=beam_width * num_prompts,
577+
max_seq_len=64,
578+
kv_cache_config=KvCacheConfig(max_tokens=10000),
579+
disable_overlap_scheduler=True,
580+
cuda_graph_config=None,
581+
)
582+
with llm:
583+
sampling_params = SamplingParams(
584+
max_tokens=16,
585+
n=beam_width,
586+
best_of=beam_width,
587+
use_beam_search=True,
588+
beam_search_diversity_rate=0.5,
589+
early_stopping=1,
590+
length_penalty=0.5,
591+
end_id=end_id,
592+
)
593+
outputs = llm.generate(deepcopy(input_prompts),
594+
sampling_params=deepcopy(sampling_params))
595+
596+
assert isinstance(outputs, list)
597+
assert len(outputs) == num_prompts
598+
for output in outputs:
599+
beams = output.outputs
600+
assert len(beams) == beam_width, (
601+
f"expected {beam_width} beams, but got {len(beams)}")
602+
beam_sequences = []
603+
for beam_idx, beam in enumerate(beams):
604+
token_ids = beam.token_ids
605+
assert token_ids is not None, f"beam {beam_idx} has no token_ids"
606+
assert len(token_ids) > 0, f"beam {beam_idx} is empty"
607+
assert all(0 <= t < vocab_size for t in token_ids), (
608+
f"beam {beam_idx} has out-of-vocab tokens: {token_ids}")
609+
beam_sequences.append(tuple(token_ids))
610+
# Beams must not all collapse to the same sequence: corrupted state
611+
# produced duplicated / garbled beams. With a non-zero diversity rate
612+
# the beams are expected to differ.
613+
assert len(set(beam_sequences)) > 1, (
614+
f"all {beam_width} beams are identical: {beam_sequences[0]}")
615+
616+
546617
###########################################################################
547618
# Unit tests
548619
###########################################################################

0 commit comments

Comments
 (0)