Skip to content

Add TDT loss kernel - #46048

Draft
ebezzam wants to merge 95 commits into
huggingface:mainfrom
ebezzam:tdt_loss_kernel
Draft

ebezzam wants to merge 95 commits into
huggingface:mainfrom
ebezzam:tdt_loss_kernel

Conversation

@ebezzam

@ebezzam ebezzam commented May 19, 2026 •

Copy link
Copy Markdown
Contributor

🤗 Transformers CI

What does this PR do?

Use the kernels-community/tdt-loss CUDA kernel for the TDT loss of ParakeetForTDT, for faster training.

The kernel is swapped in with the usual kernels exchange pattern (as for rotary_pos_emb):

  • tdt_loss is decorated with use_kernel_forward_from_hub("tdt_loss") and attached to ParakeetForTDT with use_kernelized_func
  • the tdt_loss mapping points to the TDTLoss layer of kernels-community/tdt-loss (version 1), for training and inference, including torch.compile
  • so it is used with ParakeetForTDT.from_pretrained(..., use_kernels=True); the PyTorch implementation stays the default

Speed (loss fwd+bwd, batch 4, 200 frames, 40 labels, vocab 8193): 1259 ms → 9.5 ms on A100, 993 ms → 5.0 ms on H200. Fine-tuning parakeet-tdt-0.6b-v3 on LibriSpeech gives the same loss curve as the PyTorch loss, with a full training step going from 1882 ms to 213 ms on A100 and peak memory from 33.6 to 21.8 GiB.

Tests: ParakeetForTDTModelTest.test_use_kernels checks that use_kernels=True swaps tdt_loss for the kernel (in inference and training) with the same loss. The kernel's correctness (against the PyTorch loss, a naive loop version and float64) is tested in kernels-community.

Merge order: this needs kernels-community/tdt-loss to be published first (huggingface/kernels-community#882), otherwise use_kernels=True can't load it. Tested on GPU with the staged build of the kernel (kernels-staging/tdt-loss@pr-882).

cc @eustlb @vasqu

🤖 Generated with Claude Code

Hainan Xu and others added 30 commits February 20, 2026 09:45
Implement Token-and-Duration Transducer (TDT) decoding for Parakeet models,
extending the existing CTC-only support. This adds ParakeetForTDT with greedy
TDT decoding in generate(), per-token timestamp generation, and full
integration with AutoModelForTDT, processors, and ASR pipeline.
- Use -100 label padding for training (HF convention)
- Fix timestamp recording in inner blank-seeking loop
- Add max_symbols_per_step guard matching NeMo
- Clean up decoding loop
- Add TDT training example to docs
- Use setUpClass for TDT integration tests
Comment on lines 736 to 738
Verify that ParakeetForTDT loss matches NeMo's TDT loss (sigma=0) for both
the CUDA kernel and the pure PyTorch implementation.
reproducer: https://gist.github.com/883ea42bf7d8ce2af42f3055627476a7

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also test for sigma != 0?

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Deep-unlearning and others added 6 commits September 28, 2026 14:36
# Conflicts:
#	src/transformers/integrations/hub_kernels.py
#	tests/models/parakeet/test_modeling_parakeet.py
- Point the hub kernel mapping to kernels-community/tdt-loss (version 1).
- Dispatch to the kernel for CUDA inputs, for all reductions and sigma values.
- Test the kernel against the NeMo fixtures and the PyTorch implementation
  (including sigma != 0), and run the integration loss test with both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Train mode updates the BatchNorm running statistics, which changed the eval
loss of the second backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntegration test loops

- Any error while loading the kernel (e.g. the Hub is unreachable) now falls
  back to the PyTorch implementation instead of failing the loss.
- Remove a duplicated forward/backward in the integration test and free the
  outputs inside each backend's subtest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decorate `tdt_loss` with `use_kernel_forward_from_hub` and attach it to
ParakeetForTDT with `use_kernelized_func`, so that `use_kernels=True`
swaps in the `TDTLoss` layer of kernels-community/tdt-loss (as for rotary),
instead of loading the kernel automatically.
},
},
"tdt_loss": {
"cuda": LayerRepository(repo_id="kernels-community/tdt-loss", layer_name="TDTLoss", version=1),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc your kernel did not support compile right? lets give proper flags

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it supports backward but not torch.compile (can_torch_compile = False on the layer). Done in 3f02948: the mapping now registers it for Mode.TRAINING and Mode.INFERENCE only.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo no need to test it on this side as the kernel should test it for itself no? Meaning kernels community ensures that the loss is correct 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, the kernel tests in kernels-community already check it against a PyTorch reference, a naive loop version and float64. Done in 3f02948: I removed the loss-correctness tests here and restored the integration test, keeping only test_use_kernels, which checks that use_kernels=True actually swaps tdt_loss for the kernel (inference and training) with the same loss.

Also tested against your staged build (kernels-staging/tdt-loss@pr-882) on an L4: the kernels-community tests pass (43), the Parakeet tests pass with use_kernels=True (including the slow integration test and the full test file), and the speed matches my earlier numbers (loss fwd+bwd 1285 ms → 53 ms at Parakeet size).

… the kernel exchange test

- Register the kernel for training and inference only (it does not support torch.compile).
- The kernel's correctness is tested in kernels-community: keep only the test that
  use_kernels=True swaps tdt_loss for it, and restore the integration test.
@require_torch
@require_torch_gpu
@require_kernels
class TDTLossKernelTest(unittest.TestCase):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets make this a test under the model instead of a separate class pls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5bd9cac: test_use_kernels is now a test of ParakeetForTDTModelTest.

),
},
},
"tdt_loss": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might have to drop the kernel for when we make use kernels true as default. best to make it toch compile friendly from the get go

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, done. The kernel now supports torch.compile (huggingface/kernels-community#882, 02647af): fake implementations for its ops, can_torch_compile = True on the layer, and tests with torch.compile(fullgraph=True) (fp32/bf16, forward and backward match eager) and torch.library.opcheck. Here, the mapping is now Mode.TRAINING | Mode.TORCH_COMPILE (5bd9cac), like the liger loss.

Checked on GPU with a build of the new kernel: a ParakeetForTDT kernelized with Mode.TRAINING | Mode.TORCH_COMPILE and a compiled tdt_loss match eager, and the Parakeet tests pass.

Comment on lines +60 to +65
def reset_parakeet_kernels():
"""`use_kernels=True` replaces the forward of the shared kernelized functions: restore the PyTorch ones."""
from transformers.models.parakeet import modeling_parakeet

for fn in (tdt_loss, modeling_parakeet.apply_rotary_pos_emb):
vars(fn).pop("forward", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we already have preserve_module_forwards let's use that instead. This is super dependent on only certain parts being kernelized

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0a4ccb2: removed the helper and wrapped the kernelized parts of test_use_kernels in preserve_module_forwards(model) (also checking that tdt_loss is restored afterwards).


@require_torch_gpu
@require_kernels
def test_use_kernels(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets make the split as in create_and_check_model / test_model then we can merge. Note that when kernels = True lands that we will temporarily disable training kernels as we need to make it compatible with the trainer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 513ea4b: the logic is now in ParakeetForTDTModelTester.create_and_check_use_kernels (with prepare_config_and_inputs_for_loss), and test_use_kernels just calls it, as for test_model. Thanks for the heads-up on the training kernels!

Comment on lines +700 to +701
if not isinstance(tdt_loss, torch.nn.Module):
self.skipTest("Hub kernels are disabled (USE_HUB_KERNELS)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo should not be needed because we will change the logic in any case so the function is always implicitly a module after we default to kernels true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8c0d977.

github-actions Bot pushed a commit to huggingface/kernels-community that referenced this pull request Oct 9, 2026
## What does this PR do?

Goal of this PR is to add a
[kernel](https://huggingface.co/eustlb/tdt-loss/tree/v1) that has been
drafted by @eustlb for the recently merged [Parakeet
TDT](huggingface/transformers#44171).

It's a very rough draft with a lot of Claude help as it's my first time
working with kernels 😅

TODO
- [ ] @eustlb: I guess you had a .cu file to create the .so file
[here](https://huggingface.co/eustlb/tdt-loss/tree/v1/build/torch211-cxx11-cu128-x86_64-linux/tdt_loss)?
From what I understand, we will need to add it here.
- [ ] Add the kernel to https://huggingface.co/kernels-community
- [ ] Update the tests
- [ ] Double-check with [this
checklist](https://github.com/huggingface/kernels-community/blob/main/CONTRIBUTING.md#how-to-add-a-new-kernel)

Perhaps we can see how other kernels [used in
Transformers](https://github.com/huggingface/transformers/blob/main/src/transformers/integrations/hub_kernels.py)
were integrated. As the kernel is for a function, maybe
[rotary_pos_emb](https://github.com/huggingface/transformers/blob/0df9b7fcaab447c75543598e6d959065c2296a24/src/transformers/integrations/hub_kernels.py#L221)
is a good example?
[Here](https://github.com/huggingface/kernels-community/tree/main/rotary)
are the files in this repo.

Here is a draft PR for adding/using this kernel in Transformers:
huggingface/transformers#46048

cc @vasqu

---------

Co-authored-by: Deep-unlearning <steven@huggingface.co>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@Deep-unlearning

Copy link
Copy Markdown
Contributor

@vasqu the kernel is merged and live on the Hub (kernels-community/tdt-loss, v1) 🎉 I re-ran the Parakeet tests on an L4 against the published kernel and all pass. I also restricted test_use_kernels to the tdt_loss mapping so it doesn't depend on other kernels' builds, and fixed the Hub link. Could you take another look and trigger run-slow: parakeet when you get a chance? Thanks!

@vasqu vasqu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close to being ready 🤗 im not sure whether we want to merge this or wait until after training kernels and use_kernels=True becomes a thing because it needs to be deactivated either way

Comment thread src/transformers/loss/loss_tdt.py Outdated
Comment on lines +43 to +44
With `use_kernels=True`, CUDA models use the [`kernels-community/tdt-loss`](https://huggingface.co/kernels/kernels-community/tdt-loss)
kernel instead, which computes the same loss with fused CUDA kernels.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm not sure whether it makes sense to keep this, we need some general kernels docs in the future that just lists all possible kernels

Comment on lines +642 to +645
# Only kernelize `tdt_loss`, so that the test does not depend on the other kernels of the model
model.kernel_config = KernelConfig(
kernel_mapping={"tdt_loss": get_kernel_mapping_transformers()["tdt_loss"]}, inherit_mapping=False
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be overkill no? it should be small enough diffs to not matter

@Deep-unlearning

Copy link
Copy Markdown
Contributor

Thanks @vasqu! Removed the docstring note and the KernelConfig restriction in the test (3d23b40). On timing, happy either way: since it's opt-in, merging now lets people use the kernel already, and it's a one-line mapping change to deactivate it when use_kernels=True lands. But if you'd rather wait, that's fine too 🤗

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: parakeet

@github-actions

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 38443671700:1
Result: success | Jobs: 15 | Tests: 193,911 | Failures: 0 | Duration: 6h 33m

@vasqu

vasqu commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Then I'd rather wait for now if that's ok. Trying to keep new kernels to a minimum until everything is resolved

@vasqu

vasqu commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

But feel free to ping me and follow #47773 for updates

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants