Skip to content

Commit 782a7a9

Browse files
authored
[perf] Fun-ASR-Nano fine-tuning on B200: 0.244 s to 0.068 s per step at 1 GPU, mostly from cuDNN SDPA plan builds on every new batch shape (#3705)
* fun_asr_nano: opt-in scoped SDPA backends and compiled decoder for fine-tuning * tests: FunASRNano opt-ins leave defaults untouched * fun_asr_nano: declare the torch 2.3 floor the fine-tuning recipe needs
1 parent 1878d61 commit 782a7a9

9 files changed

Lines changed: 246 additions & 1 deletion

File tree

‎examples/industrial_data_pretraining/fun_asr_nano/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ cd Fun-ASR
5858
pip install -r requirements.txt
5959
```
6060

61+
`finetune.sh` sets `llm_conf.sdpa_backends`, which needs `torch.nn.attention`, so fine-tuning with it requires PyTorch 2.3 or newer.
62+
6163
<a name="usage-tutorial"></a>
6264

6365
# TODO

‎examples/industrial_data_pretraining/fun_asr_nano/README_zh.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ cd Fun-ASR
5858
pip install -r requirements.txt
5959
```
6060

61+
`finetune.sh` 会设置 `llm_conf.sdpa_backends`,该选项依赖 `torch.nn.attention`,因此使用它进行微调需要 PyTorch 2.3 或更高版本。
62+
6163
<a name="用法教程"></a>
6264

6365
# TODO

‎examples/industrial_data_pretraining/fun_asr_nano/docs/finetune.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ For more detailed parameters, refer to: [SenseVoice Model Training and Testing](
8787
bash finetune.sh
8888
```
8989

90+
### Training-speed options
91+
92+
Two `llm_conf` keys, both off by default; `finetune.sh` turns them on.
93+
94+
- `++llm_conf.torch_compile=true` runs the LLM decoder stack through `torch.compile(dynamic=True)` for inputs on a CUDA GPU (a CPU decoder and single-sequence batches stay eager); the first compiled step pays a one-time compile.
95+
- `++llm_conf.sdpa_backends=[flash,efficient,math]` runs the LLM forward under `torch.nn.attention.sdpa_kernel` with these attention backends. It matters on sm_90 / sm_100 GPUs, where torch prefers cuDNN and cuDNN builds an execution plan for every new batch shape. The flags it sets are process-wide while the forward runs (restored on return), so a thread running attention concurrently in the same process sees the same selection; leave it unset when several models share one process.
96+
9097
### Recommended Configuration
9198

9299
- For training data less than 1000 hours, it is recommended to fine-tune the audio_adaptor.

‎examples/industrial_data_pretraining/fun_asr_nano/finetune.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,6 @@ ${train_tool} \
6262
++audio_encoder_conf.freeze=true \
6363
++audio_adaptor_conf.freeze=true \
6464
++llm_conf.freeze=false \
65+
++llm_conf.torch_compile=true \
66+
++llm_conf.sdpa_backends=[flash,efficient,math] \
6567
++output_dir="${output_dir}" &> ${log_file}

‎examples/industrial_data_pretraining/fun_asr_nano/model.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
normalize_checkpoint_state,
2121
)
2222
from funasr.models.fun_asr_nano.device_utils import resolve_autocast_device_type
23+
from funasr.models.fun_asr_nano.llm_forward_opts import configure_llm_forward
2324

2425

2526
from ctc import CTC
@@ -93,6 +94,11 @@ def __init__(
9394

9495
self.llm_dtype = llm_conf.get("llm_dtype", "fp32")
9596
self.llm = model.to(dtype_map[self.llm_dtype])
97+
# Opt-in training-speed switches, both off by default (finetune.sh turns them on):
98+
# llm_conf.sdpa_backends sets the process-wide SDPA backend flags while this forward
99+
# runs and restores them on return; llm_conf.torch_compile compiles the decoder stack
100+
# for inputs on a CUDA device (a CPU decoder stays eager). See llm_forward_opts.py.
101+
configure_llm_forward(self.llm, llm_conf)
96102
llm_dim = model.get_input_embeddings().weight.shape[-1]
97103

98104
# adaptor

‎examples/industrial_data_pretraining/fun_asr_nano/requirements.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
torch>=2.0
1+
torch>=2.3
22
funasr
33
websockets>=12.0
44
regex
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Opt-in switches for the Fun-ASR-Nano LLM forward, read from ``llm_conf``; both default to off.
2+
3+
``llm_conf.sdpa_backends``: a list of ``flash`` / ``efficient`` / ``math``. The decoder forward runs
4+
under ``torch.nn.attention.sdpa_kernel`` with these backends: the process-wide SDPA flags are set on
5+
entry and restored on return, so another thread calling scaled_dot_product_attention meanwhile sees
6+
the same selection (leave it unset when several models share one process concurrently).
7+
``llm_conf.torch_compile``: run the decoder stack through ``torch.compile(dynamic=True)`` for
8+
inputs on a CUDA device, decided per call; a CPU decoder and a 1-sequence batch stay eager.
9+
"""
10+
11+
import torch
12+
13+
SDPA_BACKENDS = {"flash": "FLASH_ATTENTION", "efficient": "EFFICIENT_ATTENTION", "math": "MATH"}
14+
15+
16+
def resolve_sdpa_backends(names):
17+
"""``llm_conf.sdpa_backends`` (a list of names, or ``None``) -> ``SDPBackend`` members."""
18+
if names is None:
19+
return None
20+
from torch.nn.attention import SDPBackend # a pybind enum: getattr, not subscript
21+
22+
backends = []
23+
for name in names:
24+
member = SDPA_BACKENDS.get(name.lower())
25+
if member is None:
26+
raise ValueError(
27+
f"llm_conf.sdpa_backends: unknown backend {name!r}, "
28+
f"choose from {sorted(SDPA_BACKENDS)}"
29+
)
30+
backends.append(getattr(SDPBackend, member))
31+
return backends
32+
33+
34+
def configure_llm_forward(llm, llm_conf):
35+
"""Install the switches ``llm_conf`` asks for on ``llm.model.forward`` (nothing by default)."""
36+
backends = resolve_sdpa_backends(llm_conf.get("sdpa_backends", None))
37+
compile_requested = bool(llm_conf.get("torch_compile", False))
38+
if backends is None and not compile_requested:
39+
return
40+
41+
decoder = llm.model
42+
forward = eager = decoder.forward
43+
44+
if compile_requested:
45+
compiled = torch.compile(eager, dynamic=True)
46+
47+
def forward(*args, **kw):
48+
# Per call, not at construction: the trainer builds the model on the CPU and moves it
49+
# to the GPU afterwards. A 1-sequence batch would get its own specialised graph.
50+
x = kw.get("inputs_embeds", kw.get("input_ids"))
51+
if x is None and args:
52+
x = args[0]
53+
if x is None or x.device.type != "cuda" or x.shape[0] == 1:
54+
return eager(*args, **kw)
55+
return compiled(*args, **kw)
56+
57+
if backends is not None:
58+
from torch.nn.attention import sdpa_kernel
59+
60+
inner = forward
61+
62+
def forward(*args, **kw):
63+
with sdpa_kernel(backends): # wraps the compiled call, so Dynamo traces under it
64+
return inner(*args, **kw)
65+
66+
decoder.forward = forward

‎funasr/models/fun_asr_nano/model.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .ctc import CTC
2525
from .checkpoint_utils import disable_incomplete_ctc, normalize_checkpoint_state
2626
from .device_utils import resolve_autocast_device_type
27+
from .llm_forward_opts import configure_llm_forward
2728
from .tools.utils import forced_align
2829

2930
dtype_map = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}
@@ -127,6 +128,11 @@ def __init__(
127128

128129
self.llm_dtype = llm_conf.get("llm_dtype", "fp32")
129130
self.llm = model.to(dtype_map[self.llm_dtype])
131+
# Opt-in training-speed switches, both off by default (finetune.sh turns them on):
132+
# llm_conf.sdpa_backends sets the process-wide SDPA backend flags while this forward
133+
# runs and restores them on return; llm_conf.torch_compile compiles the decoder stack
134+
# for inputs on a CUDA device (a CPU decoder stays eager). See llm_forward_opts.py.
135+
configure_llm_forward(self.llm, llm_conf)
130136
llm_dim = model.get_input_embeddings().weight.shape[-1]
131137

132138
# lora: inject LoRA adapters into the LLM target Linear layers
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""llm_conf.sdpa_backends / llm_conf.torch_compile on both copies of FunASRNano (the package
2+
class and the recipe's model.py, imported by path). CPU-only, no weights: tiny stand-ins."""
3+
4+
import importlib.util
5+
import os
6+
import re
7+
import sys
8+
import types
9+
10+
import pytest
11+
import torch
12+
import torch.nn as nn
13+
14+
from funasr.models.fun_asr_nano import model as funasr_model
15+
from funasr.register import tables
16+
17+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18+
RECIPE_DIR = os.path.join(REPO, "examples", "industrial_data_pretraining", "fun_asr_nano")
19+
20+
21+
class _TinyDecoder(nn.Module):
22+
"""Stands in for the HF decoder stack (``llm.model``); records every eager call."""
23+
24+
def __init__(self, dim):
25+
super().__init__()
26+
self.proj = nn.Linear(dim, dim)
27+
self.eager_calls = []
28+
29+
def forward(self, input_ids=None, inputs_embeds=None, **kwargs):
30+
if not torch.compiler.is_compiling():
31+
cudnn = torch.backends.cuda.cudnn_sdp_enabled()
32+
self.eager_calls.append({"batch": int(inputs_embeds.shape[0]), "cudnn": cudnn})
33+
return self.proj(inputs_embeds)
34+
35+
36+
class _TinyLLM(nn.Module):
37+
def __init__(self, dim=8, vocab=16):
38+
super().__init__()
39+
self.model, self.embed = _TinyDecoder(dim), nn.Embedding(vocab, dim)
40+
41+
def get_input_embeddings(self):
42+
return self.embed
43+
44+
45+
class _TinyEncoder(nn.Module):
46+
def __init__(self, input_size=80, **kwargs):
47+
super().__init__()
48+
self.lin = nn.Linear(input_size, 4)
49+
50+
def output_size(self):
51+
return 4
52+
53+
54+
class _TinyAdaptor(nn.Module):
55+
def __init__(self, encoder_dim=4, llm_dim=8, **kwargs):
56+
super().__init__()
57+
self.lin = nn.Linear(encoder_dim, llm_dim)
58+
59+
60+
def _load_recipe_model():
61+
previous = tables.model_classes.get("FunASRNano")
62+
if RECIPE_DIR not in sys.path:
63+
sys.path.insert(0, RECIPE_DIR) # the recipe imports ``ctc`` and ``tools.utils`` bare
64+
path = os.path.join(RECIPE_DIR, "model.py")
65+
spec = importlib.util.spec_from_file_location("fun_asr_nano_recipe_model", path)
66+
module = importlib.util.module_from_spec(spec)
67+
sys.modules[spec.name] = module # tables.register() calls inspect.getfile on the class
68+
try:
69+
spec.loader.exec_module(module)
70+
except ImportError as exc:
71+
sys.modules.pop(spec.name, None)
72+
pytest.skip(f"recipe model.py not importable here: {exc}")
73+
finally:
74+
tables.model_classes["FunASRNano"] = previous # the package class, registered at import
75+
return module.FunASRNano
76+
77+
78+
@pytest.fixture(scope="module", params=["funasr", "recipe"])
79+
def model_class(request):
80+
return funasr_model.FunASRNano if request.param == "funasr" else _load_recipe_model()
81+
82+
83+
@pytest.fixture
84+
def build(monkeypatch, model_class):
85+
"""``build(llm_conf) -> FunASRNano`` on the CPU, with the transformers loaders stubbed."""
86+
fake = types.ModuleType("transformers")
87+
fake.AutoConfig = types.SimpleNamespace(from_pretrained=lambda path, **kw: {})
88+
fake.AutoModelForCausalLM = types.SimpleNamespace(from_config=lambda config, **kw: _TinyLLM())
89+
monkeypatch.setattr(funasr_model, "AutoConfig", fake.AutoConfig) # bound at import time
90+
monkeypatch.setattr(funasr_model, "AutoModelForCausalLM", fake.AutoModelForCausalLM)
91+
monkeypatch.setitem(sys.modules, "transformers", fake) # the recipe imports in __init__
92+
monkeypatch.setitem(tables.encoder_classes, "TinyEnc", _TinyEncoder)
93+
monkeypatch.setitem(tables.adaptor_classes, "TinyAdp", _TinyAdaptor)
94+
kw = dict(audio_encoder="TinyEnc", audio_adaptor="TinyAdp", llm="tiny")
95+
return lambda c: model_class(audio_encoder_conf={}, audio_adaptor_conf={}, llm_conf=c, **kw)
96+
97+
98+
def _run(model, batch):
99+
device = next(model.llm.parameters()).device
100+
return model.llm.model.forward(inputs_embeds=torch.zeros(batch, 3, 8, device=device))
101+
102+
103+
def test_default_leaves_sdpa_flag_and_forward_alone(build):
104+
flag = torch.backends.cuda.cudnn_sdp_enabled()
105+
model = build({})
106+
assert torch.backends.cuda.cudnn_sdp_enabled() == flag
107+
assert "forward" not in model.llm.model.__dict__ # still the class method
108+
_run(model, 2)
109+
assert model.llm.model.eager_calls[-1]["cudnn"] == flag
110+
111+
112+
def test_sdpa_backends_disable_cudnn_only_inside_forward(build):
113+
flag = torch.backends.cuda.cudnn_sdp_enabled()
114+
model = build({"sdpa_backends": ["flash", "efficient", "math"]})
115+
assert torch.backends.cuda.cudnn_sdp_enabled() == flag # construction changed nothing
116+
_run(model, 2)
117+
assert model.llm.model.eager_calls[-1]["cudnn"] is False
118+
assert torch.backends.cuda.cudnn_sdp_enabled() == flag # restored on return
119+
120+
121+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a CUDA device")
122+
def test_torch_compile_wraps_cuda_llm_and_keeps_batch_one_eager(build):
123+
model = build({"torch_compile": True}).cuda() # built on the CPU and moved, as the trainer does
124+
decoder = model.llm.model
125+
assert "forward" in decoder.__dict__
126+
_run(model, 1)
127+
assert [c["batch"] for c in decoder.eager_calls] == [1]
128+
out = _run(model, 2) # compiled: the stand-in does not record under Dynamo
129+
assert [c["batch"] for c in decoder.eager_calls] == [1]
130+
torch.testing.assert_close(out, decoder.proj(torch.zeros(2, 3, 8, device=out.device)))
131+
132+
133+
def test_torch_compile_keeps_cpu_decoder_eager(build):
134+
model = build({"torch_compile": True})
135+
_run(model, 1)
136+
_run(model, 4)
137+
assert [c["batch"] for c in model.llm.model.eager_calls] == [1, 4]
138+
139+
140+
def test_recipe_declares_the_torch_floor_its_options_need():
141+
"""sdpa_backends goes through torch.nn.attention, absent before torch 2.3; if a recipe
142+
turns it on, the example's requirements.txt has to say so."""
143+
for recipe in ("finetune.sh", "lora_finetune.sh"):
144+
path = os.path.join(RECIPE_DIR, recipe)
145+
if not os.path.exists(path):
146+
continue
147+
with open(path) as f:
148+
if "llm_conf.sdpa_backends" not in f.read():
149+
continue
150+
with open(os.path.join(RECIPE_DIR, "requirements.txt")) as f:
151+
declared = re.search(r"^torch>=(\d+)\.(\d+)", f.read(), re.M)
152+
assert declared, f"{recipe} sets sdpa_backends but requirements.txt pins no torch floor"
153+
floor = (int(declared.group(1)), int(declared.group(2)))
154+
assert floor >= (2, 3), f"{recipe} sets sdpa_backends; torch floor is {floor}, needs >= 2.3"

0 commit comments

Comments
 (0)