Skip to content

Commit d3a09dd

Browse files
revert the k2 borrowing
Signed-off-by: MahmoudAshraf97 <hassouna97.ma@gmail.com>
1 parent 76a166d commit d3a09dd

6 files changed

Lines changed: 145 additions & 458 deletions

File tree

nemo/collections/asr/parts/k2/graph_transducer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from nemo.utils import logging
2626

2727
if TRITON_AVAILABLE:
28-
from nemo.collections.asr.parts.triton.rnnt_logprobs import rnnt_logprobs_triton
28+
from nemo.collections.asr.parts.k2.rnnt_logprobs_triton import rnnt_logprobs_triton
2929

3030

3131
def force_float32_context() -> ContextManager:
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import torch
16+
import torch.nn.functional as F
17+
18+
19+
def rnnt_logprobs_torch(
20+
logits: torch.Tensor, targets: torch.Tensor, blank_id: int
21+
) -> tuple[torch.Tensor, torch.Tensor]:
22+
"""
23+
Given logits, calculate log probabilities for blank and target labels needed for transducer loss calculation.
24+
Naive implementation in PyTorch, for testing and prototyping purposes.
25+
26+
Args:
27+
logits: Joint tensor of size [B, T, U+1, D]
28+
targets: Targets of size [B, U]
29+
blank_id: id of the blank output
30+
31+
Returns:
32+
Tuple of tensors with log probabilities for targets and blank labels, both of size [B, T, U+1].
33+
For the last non-existent target (U+1) output is zero.
34+
"""
35+
device = logits.device
36+
batch_size = logits.shape[0]
37+
log_probs = F.log_softmax(logits, dim=-1)
38+
blank_scores = log_probs[..., blank_id]
39+
targets = torch.cat((targets, torch.zeros([batch_size], dtype=targets.dtype, device=device).unsqueeze(1)), dim=-1)
40+
target_scores = torch.gather(
41+
log_probs, dim=-1, index=targets.unsqueeze(1).expand(log_probs.shape[:-1]).unsqueeze(-1)
42+
).squeeze(-1)
43+
target_scores[:, :, -1] = 0.0
44+
return target_scores, blank_scores

nemo/collections/asr/parts/triton/rnnt_logprobs.py renamed to nemo/collections/asr/parts/k2/rnnt_logprobs_triton.py

Lines changed: 17 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# limitations under the License.
1414

1515
import torch
16-
import torch.nn.functional as F
1716
import triton
1817
import triton.language as tl
1918

@@ -82,9 +81,6 @@ def _rnnt_logprobs_bwd_kernel(
8281
blank_id: int,
8382
grad_target_scores_ptr,
8483
grad_blank_scores_ptr,
85-
loss_grad_scale_ptr,
86-
clamp: float,
87-
CLAMP_GRAD: tl.constexpr,
8884
BLOCK_SIZE: tl.constexpr,
8985
):
9086
"""
@@ -99,7 +95,9 @@ def _rnnt_logprobs_bwd_kernel(
9995
# load lengths for source/target
10096
source_len = tl.load(source_lengths_ptr + batch_i)
10197
target_len = tl.load(target_lengths_ptr + batch_i)
102-
valid_state = (source_i < source_len) & (target_i <= target_len)
98+
if source_i >= source_len or target_i > target_len:
99+
# no calculations required
100+
return
103101

104102
# calculate offset in [B, T, U+1, V] tensor for the current vector with target logits/grad_logits
105103
flat_index = ((batch_i * max_source_len + source_i) * max_target_len_plus_1 + target_i) * num_labels
@@ -108,77 +106,27 @@ def _rnnt_logprobs_bwd_kernel(
108106

109107
col_offsets = tl.arange(0, BLOCK_SIZE)
110108
mask = col_offsets < num_labels
111-
logits = tl.load(logits_ptr + col_offsets, mask=mask & valid_state, other=-float("inf")).to(tl.float32)
109+
logits = tl.load(logits_ptr + col_offsets, mask=mask, other=-float("inf")).to(tl.float32)
112110
# stable log softmax calculation
113111
logits_max = tl.max(logits, axis=0)
114112
logits_minus_max = logits - logits_max
115-
unnormalized = tl.exp(logits_minus_max)
116-
denominator_sum = tl.sum(unnormalized, axis=0)
113+
denominator = tl.log(tl.sum(tl.exp(logits_minus_max), axis=0))
114+
log_softmax = logits_minus_max - denominator
117115
# softmax for gradient
118-
softmax = unnormalized / denominator_sum
116+
softmax = tl.exp(log_softmax)
119117

120118
flat_index_grad = (batch_i * max_source_len + source_i) * max_target_len_plus_1 + target_i
121-
blank_grad = tl.load(grad_blank_scores_ptr + flat_index_grad, mask=valid_state, other=0.0).to(tl.float32)
122-
target_i_valid = valid_state & (target_i < target_len)
119+
blank_grad = tl.load(grad_blank_scores_ptr + flat_index_grad).to(tl.float32)
120+
target_i_valid = target_i < target_len
123121
target_grad = tl.load(grad_target_scores_ptr + flat_index_grad, mask=target_i_valid, other=0.0).to(tl.float32)
124122
target_id = tl.load(targets_ptr + batch_i * (max_target_len_plus_1 - 1) + target_i, mask=target_i_valid, other=-1)
125123

126-
if CLAMP_GRAD:
127-
# The reference kernel clamps the per-sample gradient at unit scale, before the loss
128-
# reduction or AMP scale reaches it, so divide that scale out, clamp, and reapply below.
129-
loss_grad_scale = tl.load(loss_grad_scale_ptr + batch_i).to(tl.float32)
130-
inverse_scale = tl.where(loss_grad_scale != 0.0, 1.0 / loss_grad_scale, 0.0)
131-
blank_grad *= inverse_scale
132-
target_grad *= inverse_scale
133-
134124
grad_not_in_targets = (-softmax) * (blank_grad + target_grad)
135-
# Add both deltas instead of overwriting one with the other. This also keeps
136-
# malformed target==blank inputs mathematically correct.
137-
grad = grad_not_in_targets
138-
grad += tl.where(col_offsets == blank_id, blank_grad, 0.0)
139-
grad += tl.where(col_offsets == target_id, target_grad, 0.0)
140-
if CLAMP_GRAD:
141-
grad = tl.maximum(tl.minimum(grad, clamp), -clamp)
142-
grad *= loss_grad_scale
143-
grad = tl.where(valid_state, grad, 0.0)
125+
grad = tl.where(col_offsets == blank_id, blank_grad + grad_not_in_targets, grad_not_in_targets)
126+
grad = tl.where(col_offsets == target_id, target_grad + grad_not_in_targets, grad)
144127
tl.store(grad_logits_ptr + col_offsets, grad, mask=mask)
145128

146129

147-
def _validate_rnnt_logprobs_inputs(
148-
logits: torch.Tensor,
149-
targets: torch.Tensor,
150-
blank_id: int,
151-
source_lengths: torch.Tensor | None,
152-
target_lengths: torch.Tensor | None,
153-
) -> None:
154-
"""Validate the tensor layout assumed by the pointer arithmetic below."""
155-
if logits.ndim != 4:
156-
raise ValueError(f"logits must have shape [B, T, U + 1, V], got {tuple(logits.shape)}")
157-
if targets.ndim != 2:
158-
raise ValueError(f"targets must have shape [B, U], got {tuple(targets.shape)}")
159-
expected_targets = (logits.shape[0], logits.shape[2] - 1)
160-
if targets.shape != expected_targets:
161-
raise ValueError(f"targets must have shape {expected_targets}, got {tuple(targets.shape)}")
162-
if not 0 <= blank_id < logits.shape[-1]:
163-
raise ValueError(f"blank_id={blank_id} must be in [0, {logits.shape[-1]})")
164-
if not logits.is_contiguous():
165-
raise ValueError("logits must be contiguous")
166-
if targets.device != logits.device:
167-
raise ValueError("targets and logits must be on the same device")
168-
if targets.dtype not in (torch.int32, torch.int64):
169-
raise ValueError("targets must use int32 or int64 indices")
170-
171-
for name, lengths in (("source_lengths", source_lengths), ("target_lengths", target_lengths)):
172-
if lengths is None:
173-
continue
174-
if lengths.shape != (logits.shape[0],):
175-
raise ValueError(f"{name} must have shape ({logits.shape[0]},), got {tuple(lengths.shape)}")
176-
if lengths.device != logits.device:
177-
raise ValueError(f"{name} and logits must be on the same device")
178-
if lengths.dtype not in (torch.int32, torch.int64):
179-
raise ValueError(f"{name} must use int32 or int64 values")
180-
181-
182130
class RnntLogProbs(torch.autograd.Function):
183131
"""
184132
Function to calculate log probabilities for target and blank labels for RNN-T, supporting torch.autograd.
@@ -192,11 +140,8 @@ def forward(
192140
blank_id: int,
193141
source_lengths: torch.Tensor | None,
194142
target_lengths: torch.Tensor | None,
195-
clamp: float,
196-
reuse_logits_for_grad: bool,
197-
loss_grad_scale: torch.Tensor | None,
198143
):
199-
"""Log probabilities of the target and blank labels at every lattice position.
144+
"""
200145
201146
Args:
202147
ctx: ctx object for storing the context
@@ -205,13 +150,11 @@ def forward(
205150
blank_id: id of the blank output
206151
source_lengths: optional tensor with lengths for source utterances
207152
target_lengths: optional tensor with lengths for targets
208-
clamp: bound on the unit-scale gradient, disabled when not positive
209-
reuse_logits_for_grad: write the gradient over ``logits`` rather than allocating for it
210-
loss_grad_scale: per-sample loss gradient the clamp divides out and reapplies
211153
212154
Returns:
213-
Log probabilities for target and blank labels, both of size [B, T, U+1].
155+
214156
"""
157+
assert logits.is_contiguous() # logits are huge, so here we just check if logits are contiguous
215158
targets = targets.contiguous()
216159
device = logits.device
217160
float_dtype = torch.float32
@@ -247,12 +190,6 @@ def forward(
247190
# saving for backward
248191
ctx.save_for_backward(logits, targets, source_lengths, target_lengths)
249192
ctx.blank_id = blank_id
250-
ctx.clamp = float(clamp) if clamp > 0.0 else 0.0
251-
ctx.reuse_logits_for_grad = reuse_logits_for_grad
252-
ctx.reused_logits_consumed = False
253-
# Held outside save_for_backward: the loss fills it during its own backward, which
254-
# runs before ours because these scores are what it consumes.
255-
ctx.loss_grad_scale = loss_grad_scale
256193
return target_scores, blank_scores
257194

258195
@staticmethod
@@ -268,19 +205,9 @@ def backward(ctx, grad_target_scores, grad_blank_scores):
268205
Returns:
269206
gradient for logits, None for all other arguments for `forward`
270207
"""
271-
if ctx.reuse_logits_for_grad:
272-
if ctx.reused_logits_consumed:
273-
raise RuntimeError("reuse_logits_for_grad=True only supports one backward pass")
274-
ctx.reused_logits_consumed = True
275208
(logits, targets, source_lengths, target_lengths) = ctx.saved_tensors
276209
blank_id = ctx.blank_id
277-
clamp = ctx.clamp
278-
grad_target_scores = grad_target_scores.contiguous()
279-
grad_blank_scores = grad_blank_scores.contiguous()
280-
grad_logits = logits if ctx.reuse_logits_for_grad else torch.zeros_like(logits)
281-
# Any valid pointer will do when clamping is off: CLAMP_GRAD is a constexpr, so the
282-
# only branch that reads this argument is compiled out.
283-
loss_grad_scale = ctx.loss_grad_scale if ctx.loss_grad_scale is not None else grad_blank_scores
210+
grad_logits = torch.zeros_like(logits)
284211
_rnnt_logprobs_bwd_kernel[(logits.shape[0], logits.shape[1], logits.shape[2])](
285212
logits_ptr=logits,
286213
grad_logits_ptr=grad_logits,
@@ -293,12 +220,9 @@ def backward(ctx, grad_target_scores, grad_blank_scores):
293220
blank_id=blank_id,
294221
grad_target_scores_ptr=grad_target_scores,
295222
grad_blank_scores_ptr=grad_blank_scores,
296-
loss_grad_scale_ptr=loss_grad_scale,
297-
clamp=clamp,
298-
CLAMP_GRAD=clamp > 0.0,
299223
BLOCK_SIZE=triton.next_power_of_2(logits.shape[-1]),
300224
)
301-
return grad_logits, None, None, None, None, None, None, None
225+
return grad_logits, None, None, None, None
302226

303227

304228
def rnnt_logprobs_triton(
@@ -307,9 +231,6 @@ def rnnt_logprobs_triton(
307231
blank_id: int,
308232
source_lengths: torch.Tensor | None = None,
309233
target_lengths: torch.Tensor | None = None,
310-
clamp: float = -1.0,
311-
reuse_logits_for_grad: bool = False,
312-
loss_grad_scale: torch.Tensor | None = None,
313234
) -> tuple[torch.Tensor, torch.Tensor]:
314235
"""
315236
Given logits, calculate log probabilities for blank and target labels needed for transducer loss calculation.
@@ -321,56 +242,9 @@ def rnnt_logprobs_triton(
321242
blank_id: id of the blank output
322243
source_lengths: optional tensor with lengths for source utterances
323244
target_lengths: optional tensor with lengths for targets
324-
clamp: bound on the unit-scale RNN-T gradient, applied before ``loss_grad_scale`` is
325-
reapplied; disabled when not positive
326-
reuse_logits_for_grad: overwrite logits with their gradient during backward; only safe for private,
327-
disposable logits; a second backward through the same graph raises
328-
loss_grad_scale: ``[B]`` float32 buffer holding the objective's gradient with respect to
329-
each per-sample loss, filled by ``rnnt_loss_triton``. Required only when clamping: the
330-
clamp bounds the unit-scale gradient, and by this point autograd has folded that scale
331-
in, so the backward divides it out, clamps, and reapplies it.
332245
333246
Returns:
334247
Tuple of tensors with log probabilities for targets and blank labels, both of size [B, T, U+1].
335248
For the non-existent targets (U+1 or beyond target_lengths) output is zero.
336249
"""
337-
_validate_rnnt_logprobs_inputs(logits, targets, blank_id, source_lengths, target_lengths)
338-
if clamp > 0.0:
339-
if loss_grad_scale is None:
340-
raise ValueError("Clamping the RNN-T gradient requires loss_grad_scale")
341-
if loss_grad_scale.shape != (logits.shape[0],) or loss_grad_scale.dtype != torch.float32:
342-
raise ValueError(
343-
f"loss_grad_scale must be a float32 tensor of shape ({logits.shape[0]},), "
344-
f"got {tuple(loss_grad_scale.shape)} of {loss_grad_scale.dtype}"
345-
)
346-
return RnntLogProbs.apply(
347-
logits, targets, blank_id, source_lengths, target_lengths, clamp, reuse_logits_for_grad, loss_grad_scale
348-
)
349-
350-
351-
def rnnt_logprobs_torch(
352-
logits: torch.Tensor, targets: torch.Tensor, blank_id: int
353-
) -> tuple[torch.Tensor, torch.Tensor]:
354-
"""
355-
Given logits, calculate log probabilities for blank and target labels needed for transducer loss calculation.
356-
Naive implementation in PyTorch, for testing and prototyping purposes.
357-
358-
Args:
359-
logits: Joint tensor of size [B, T, U+1, D]
360-
targets: Targets of size [B, U]
361-
blank_id: id of the blank output
362-
363-
Returns:
364-
Tuple of tensors with log probabilities for targets and blank labels, both of size [B, T, U+1].
365-
For the last non-existent target (U+1) output is zero.
366-
"""
367-
device = logits.device
368-
batch_size = logits.shape[0]
369-
log_probs = F.log_softmax(logits, dim=-1)
370-
blank_scores = log_probs[..., blank_id]
371-
targets = torch.cat((targets, torch.zeros([batch_size], dtype=targets.dtype, device=device).unsqueeze(1)), dim=-1)
372-
target_scores = torch.gather(
373-
log_probs, dim=-1, index=targets.unsqueeze(1).expand(log_probs.shape[:-1]).unsqueeze(-1)
374-
).squeeze(-1)
375-
target_scores[:, :, -1] = 0.0
376-
return target_scores, blank_scores
250+
return RnntLogProbs.apply(logits, targets, blank_id, source_lengths, target_lengths)

nemo/collections/asr/parts/triton/rnnt_loss.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def rnnt_loss_triton(
313313
loss_grad_scale: optional ``[B]`` float32 buffer. Backward writes ``grad_losses`` into it --
314314
the objective's gradient with respect to each per-sample loss, which the reduction and any
315315
AMP scale determine -- so a producer of the scores can recover the unit scale its own
316-
gradients were computed at. Only gradient clamping needs it; see ``rnnt_logprobs_triton``.
316+
gradients were computed at. Only gradient clamping needs it.
317317
"""
318318
losses, _, _ = _RNNTLossTriton.apply(
319319
target_scores, blank_scores, source_lengths, target_lengths, fastemit_lambda, loss_grad_scale

0 commit comments

Comments
 (0)