[Perf] Bound DiffusionGemma sampler transient via request-tiled logits (#45672)
Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com>
Guan-Ming Chiu committed
Jul 7, 2026 at 11:42 UTC
c64c3569904b6fb7fdbd4e7692ba14f4907b7275
1 file changed
+104
-90
vllm/model_executor/models/diffusion_gemma.py
+104
-90
@@ -47,6 +47,7 @@ from vllm.model_executor.models.module_mapping import MultiModelKeys
47
from vllm.model_executor.models.transformers.utils import recursive_replace_linear
48
from vllm.model_executor.models.utils import WeightsMapper, maybe_prefix
49
from vllm.multimodal import MULTIMODAL_REGISTRY
50
+from vllm.platforms import current_platform
51
from vllm.v1.outputs import LogprobsTensors
52
from vllm.v1.worker.gpu.attn_utils import build_attn_metadata
53
from vllm.v1.worker.gpu.buffer_utils import UvaBackedTensor, async_copy_to_gpu
@@ -516,11 +517,6 @@ def _compiled_sample_step(
517
num_decode = decode_slots.shape[0]
518
device = decode_slots.device
519
519
- # Clear outputs so prefill / non-decode slots report 0 (decode slots are
520
- # overwritten below).
521
- sampled.zero_()
522
- num_sampled.zero_()
523
-
520
# ---- Phase 1: Temperature schedule ----
521
steps_f = step_tensor[decode_slots].float()
522
remaining = (max_denoising_steps - steps_f).clamp(min=1.0)
@@ -1276,9 +1272,12 @@ class DiffusionSampler:
1272
src = (starts.unsqueeze(1) + ar.unsqueeze(0)).clamp_max(logits.shape[0] - 1)
1273
logits = logits[src.reshape(-1)] * valid.reshape(-1, 1).to(logits.dtype)
1274
1279
- # Cleared inside _compiled_sample_step so prefill/non-decode slots stay 0.
1275
+ # Clear once: the tiled loop below only scatters its own decode slots,
1276
+ # so it must not re-clear earlier tiles' writes.
1277
sampled = self._sampled[:num_reqs]
1278
num_sampled = self._num_sampled[:num_reqs]
1279
+ sampled.zero_()
1280
+ num_sampled.zero_()
1281
1282
all_slots = input_batch.idx_mapping[:num_reqs]
1283
@@ -1286,94 +1285,109 @@ class DiffusionSampler:
1285
# since it mutates is_encoder_phase (commit→False, converge→True).
1286
is_committing = states.is_encoder_phase[decode_slots].clone()
1287
1289
- # --- Single compiled call: temp → sample → probs → post-process ---
1290
- scaled = _compiled_sample_step(
1291
- logits,
1292
- decode_slots,
1293
- decode_idx,
1294
- all_slots,
1295
- valid_canvas_len,
1296
- # State
1297
- states.canvas,
1298
- states.argmax_canvas,
1299
- states.step,
1300
- states.is_encoder_phase,
1301
- states.confident,
1302
- states.self_conditioning_embeds,
1303
- self.embed_weight,
1304
- self.normalizer,
1305
- states.accepted_canvas_history,
1306
- states.accepted_canvas_history_len,
1307
- # Output
1308
- sampled,
1309
- num_sampled,
1310
- self.req_states.draft_tokens,
1311
- # Config
1312
- max_denoising_steps=float(states.max_denoising_steps),
1313
- t_min=self.t_min,
1314
- t_max=self.t_max,
1315
- confidence_threshold=self.confidence_threshold,
1316
- vocab_size=self.vocab_size,
1317
- CL=self.canvas_length,
1318
- ST=states.stability_threshold,
1319
- entropy_bound=self.entropy_bound,
1320
- sc_vocab_start=self.sc_vocab_start,
1321
- sc_vocab_end=self.sc_vocab_end,
1322
- tp_size=self.tp_size,
1323
- tp_group_name=self.tp_group_name,
1324
- )
1325
-
1326
- # --- Logprobs: stash on convergence, return on commit ---
1288
slots_np = input_batch.idx_mapping_np[:num_reqs]
1289
is_decode_np = per_req_nlogits_np > 0
1290
+ max_num_logprobs = self.sampling_states.max_num_logprobs(slots_np)
1291
1292
+ # Sample over the [num_decode * CL, vocab] logits. The fp32 pipeline in
1293
+ # _compiled_sample_step keeps several live [group * CL, vocab] copies, so
1294
+ # size each tile to a fraction of free memory to bound the transient at
1295
+ # high concurrency. Tiling is bit-identical to a single pass.
1296
+ group = max(num_decode, 1)
1297
+ if num_decode > 0:
1298
+ free, _ = current_platform.mem_get_info()
1299
+ # ~10 transient fp32 copies of [group * CL, vocab] inside the step
1300
+ # (eager peaks at ~8; pad for allocator overhead and small tensors).
1301
+ bytes_per_req = CL * self.vocab_size * 4 * 10
1302
+ budget = int(free * 0.5) // max(bytes_per_req, 1)
1303
+ group = max(1, min(num_decode, budget))
1304
+
1305
+ for start_req in range(0, num_decode, group):
1306
+ end_req = min(start_req + group, num_decode)
1307
+ tile = slice(start_req, end_req)
1308
+ tile_slots = decode_slots[tile]
1309
+
1310
+ scaled = _compiled_sample_step(
1311
+ logits[start_req * CL : end_req * CL],
1312
+ tile_slots,
1313
+ decode_idx[tile],
1314
+ all_slots,
1315
+ valid_canvas_len[tile],
1316
+ # State
1317
+ states.canvas,
1318
+ states.argmax_canvas,
1319
+ states.step,
1320
+ states.is_encoder_phase,
1321
+ states.confident,
1322
+ states.self_conditioning_embeds,
1323
+ self.embed_weight,
1324
+ self.normalizer,
1325
+ states.accepted_canvas_history,
1326
+ states.accepted_canvas_history_len,
1327
+ # Output
1328
+ sampled,
1329
+ num_sampled,
1330
+ self.req_states.draft_tokens,
1331
+ # Config
1332
+ max_denoising_steps=float(states.max_denoising_steps),
1333
+ t_min=self.t_min,
1334
+ t_max=self.t_max,
1335
+ confidence_threshold=self.confidence_threshold,
1336
+ vocab_size=self.vocab_size,
1337
+ CL=CL,
1338
+ ST=states.stability_threshold,
1339
+ entropy_bound=self.entropy_bound,
1340
+ sc_vocab_start=self.sc_vocab_start,
1341
+ sc_vocab_end=self.sc_vocab_end,
1342
+ tp_size=self.tp_size,
1343
+ tp_group_name=self.tp_group_name,
1344
+ )
1345
+
1346
+ # Logprobs for denoise steps that just converged (is_encoder_phase
1347
+ # flipped False→True), stashed per tile so `scaled` is freed each tile.
1348
+ if max_num_logprobs >= 0:
1349
+ converged_mask = states.is_encoder_phase[tile_slots]
1350
+ just_converged = converged_mask & ~is_committing[tile]
1351
+ if just_converged.any():
1352
+ flat_logits = scaled.reshape(-1, scaled.shape[-1])
1353
+ argmax_tokens = scaled.argmax(dim=-1)
1354
+ for local_idx in just_converged.nonzero(as_tuple=True)[0]:
1355
+ li = local_idx.item()
1356
+ slot = tile_slots[local_idx]
1357
+ # Stash only the real canvas positions (== CL unless this
1358
+ # canvas was truncated near max_model_len); padded tail
1359
+ # positions are never emitted.
1360
+ k_i = int(valid_canvas_len_np[start_req + li])
1361
+ pos = li * CL
1362
+ self._pending_logprobs[slot.item()] = compute_topk_logprobs(
1363
+ flat_logits[pos : pos + k_i],
1364
+ max_num_logprobs,
1365
+ argmax_tokens[local_idx][:k_i],
1366
+ )
1367
+
1368
+ # Commit steps: is_committing was True at entry. Reassemble previously
1369
+ # stashed logprobs and attach to SamplerOutput.
1370
logprobs_tensors = None
1331
- max_num_logprobs = self.sampling_states.max_num_logprobs(slots_np)
1332
- if max_num_logprobs >= 0:
1333
- # Denoise steps that just converged: the compiled step flipped
1334
- # is_encoder_phase from False→True. Detect as slots where
1335
- # is_encoder_phase is now True but is_committing was False.
1336
- converged_mask = states.is_encoder_phase[decode_slots]
1337
- just_converged = converged_mask & ~is_committing
1338
- if just_converged.any():
1339
- flat_logits = scaled.reshape(-1, scaled.shape[-1])
1340
- argmax_tokens = scaled.argmax(dim=-1)
1341
- for local_idx in just_converged.nonzero(as_tuple=True)[0]:
1342
- li = local_idx.item()
1343
- slot = decode_slots[local_idx]
1344
- # Stash only the real canvas positions (== CL unless this
1345
- # canvas was truncated near max_model_len); padded tail
1346
- # positions are never emitted.
1347
- k_i = int(valid_canvas_len_np[li])
1348
- start = li * CL
1349
- self._pending_logprobs[slot.item()] = compute_topk_logprobs(
1350
- flat_logits[start : start + k_i],
1351
- max_num_logprobs,
1352
- argmax_tokens[local_idx][:k_i],
1353
- )
1354
-
1355
- # Commit steps: is_committing was True at entry. Reassemble
1356
- # previously stashed logprobs and attach to SamplerOutput.
1357
- if is_committing.any() and self._pending_logprobs:
1358
- parts_ids, parts_lp, parts_ranks = [], [], []
1359
- cu_gen: list[int] = []
1360
- flat_offset = 0
1361
- for i in range(num_reqs):
1362
- cu_gen.append(flat_offset)
1363
- slot = int(slots_np[i])
1364
- if is_decode_np[i] and slot in self._pending_logprobs:
1365
- lp = self._pending_logprobs.pop(slot)
1366
- parts_ids.append(lp.logprob_token_ids)
1367
- parts_lp.append(lp.logprobs)
1368
- parts_ranks.append(lp.selected_token_ranks)
1369
- flat_offset += lp.logprobs.shape[0]
1370
- if parts_ids:
1371
- logprobs_tensors = LogprobsTensors(
1372
- logprob_token_ids=torch.cat(parts_ids),
1373
- logprobs=torch.cat(parts_lp),
1374
- selected_token_ranks=torch.cat(parts_ranks),
1375
- cu_num_generated_tokens=cu_gen,
1376
- )
1371
+ if max_num_logprobs >= 0 and is_committing.any() and self._pending_logprobs:
1372
+ parts_ids, parts_lp, parts_ranks = [], [], []
1373
+ cu_gen: list[int] = []
1374
+ flat_offset = 0
1375
+ for i in range(num_reqs):
1376
+ cu_gen.append(flat_offset)
1377
+ slot = int(slots_np[i])
1378
+ if is_decode_np[i] and slot in self._pending_logprobs:
1379
+ lp = self._pending_logprobs.pop(slot)
1380
+ parts_ids.append(lp.logprob_token_ids)
1381
+ parts_lp.append(lp.logprobs)
1382
+ parts_ranks.append(lp.selected_token_ranks)
1383
+ flat_offset += lp.logprobs.shape[0]
1384
+ if parts_ids:
1385
+ logprobs_tensors = LogprobsTensors(
1386
+ logprob_token_ids=torch.cat(parts_ids),
1387
+ logprobs=torch.cat(parts_lp),
1388
+ selected_token_ranks=torch.cat(parts_ranks),
1389
+ cu_num_generated_tokens=cu_gen,
1390
+ )
1391
1392
return self._build_output(
1393
input_batch,