Skip to content

Commit aefddc1

Browse files
authored
fix: disable timestamps when CTC weights are missing (#3211)
Fail closed for native, remote-code, and vLLM Fun-ASR-Nano timestamp inference when checkpoint CTC weights are missing, partial, shape-mismatched, DDP-prefixed, or wrapped. Fixes #3208.
1 parent 098654e commit aefddc1

8 files changed

Lines changed: 322 additions & 16 deletions

File tree

examples/industrial_data_pretraining/fun_asr_nano/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ pip install -r requirements.txt
6464
- [ ] Support speaker diarization
6565
- [x] Support model training
6666

67+
> [!NOTE]
68+
> Character-level timestamps require a checkpoint with complete `ctc_decoder.*` and
69+
> `ctc.*` weights. The current `Fun-ASR-MLT-Nano-2512` checkpoint does not include
70+
> those weights, so FunASR logs a warning and returns text without `timestamps` or
71+
> `ctc_timestamps` instead of aligning with uninitialized layers.
72+
6773
# Usage 🛠️
6874

6975
## Inference

examples/industrial_data_pretraining/fun_asr_nano/README_zh.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ pip install -r requirements.txt
6464
- [ ] 支持区分说话人识别
6565
- [x] 支持模型训练
6666

67+
> [!NOTE]
68+
> 字符级时间戳要求 checkpoint 同时包含完整的 `ctc_decoder.*``ctc.*` 权重。
69+
> 当前 `Fun-ASR-MLT-Nano-2512` checkpoint 未包含这些权重,因此 FunASR 会记录警告并仅返回文本,
70+
> 不再使用未初始化的层生成 `timestamps``ctc_timestamps`
71+
6772
# 用法 🛠️
6873

6974
## 推理

examples/industrial_data_pretraining/fun_asr_nano/model.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from funasr.train_utils.device_funcs import force_gatherable, to_device
1616
from funasr.utils.datadir_writer import DatadirWriter
1717
from funasr.utils.load_utils import extract_fbank, load_audio_text_image_video
18+
from funasr.models.fun_asr_nano.checkpoint_utils import (
19+
disable_incomplete_ctc,
20+
normalize_checkpoint_state,
21+
)
1822
from funasr.models.fun_asr_nano.device_utils import resolve_autocast_device_type
1923

2024

@@ -109,6 +113,7 @@ def __init__(
109113

110114
# ctc decoder
111115
self.ctc_decoder = None
116+
self._externally_loaded_ctc_keys = set()
112117
# TODO: fix table name
113118
ctc_decoder_class = tables.adaptor_classes.get(kwargs.get("ctc_decoder", None))
114119
if ctc_decoder_class is not None:
@@ -135,8 +140,15 @@ def __init__(
135140
self.ctc_decoder = ctc_decoder_class(**ctc_decoder_conf)
136141
init_param_path = ctc_decoder_conf.get("init_param_path", None)
137142
if init_param_path is not None:
138-
src_state = torch.load(init_param_path, map_location="cpu")
143+
src_state = normalize_checkpoint_state(
144+
torch.load(init_param_path, map_location="cpu")
145+
)
139146
flag = self.ctc_decoder.load_state_dict(src_state, strict=False)
147+
self._externally_loaded_ctc_keys.update(
148+
f"ctc_decoder.{key}"
149+
for key in self.ctc_decoder.state_dict()
150+
if key in src_state
151+
)
140152
logging.info(f"Loading ctc_decoder ckpt: {init_param_path}, status: {flag}")
141153
freeze = ctc_decoder_conf.get("freeze", False)
142154
if freeze:
@@ -160,6 +172,11 @@ def __init__(
160172
rank = int(os.environ.get("RANK", 0))
161173
logging.info(f"rank: {rank}, model is builded.")
162174

175+
def on_pretrained_model_loaded(self, loaded_keys):
176+
"""Fail closed when a checkpoint configures CTC without trained weights."""
177+
loaded_keys = set(loaded_keys).union(getattr(self, "_externally_loaded_ctc_keys", ()))
178+
disable_incomplete_ctc(self, loaded_keys, log=logging)
179+
163180
def forward(
164181
self,
165182
speech: torch.Tensor = None,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
def normalize_checkpoint_state(state):
2+
"""Unwrap common checkpoint containers and remove one DDP key prefix."""
3+
while isinstance(state, dict):
4+
wrapped = next(
5+
(
6+
state[key]
7+
for key in ("state_dict", "model_state_dict", "model")
8+
if isinstance(state.get(key), dict)
9+
),
10+
None,
11+
)
12+
if wrapped is None or wrapped is state:
13+
break
14+
state = wrapped
15+
16+
return {
17+
key[len("module.") :] if key.startswith("module.") else key: value
18+
for key, value in state.items()
19+
}
20+
21+
22+
def disable_incomplete_ctc(model, loaded_keys, *, log):
23+
"""Disable timestamp inference unless every required CTC tensor was loaded."""
24+
if model.ctc_decoder is None or model.ctc is None:
25+
return []
26+
27+
expected = {f"ctc_decoder.{key}" for key in model.ctc_decoder.state_dict()}
28+
expected.update(f"ctc.{key}" for key in model.ctc.state_dict())
29+
missing = sorted(expected.difference(loaded_keys))
30+
if not missing:
31+
return []
32+
33+
preview = ", ".join(missing[:3])
34+
suffix = "" if len(missing) <= 3 else ", ..."
35+
log.warning(
36+
"Disabling CTC timestamps because the checkpoint did not initialize "
37+
"%d of %d required CTC tensors (%s%s). Text transcription remains available.",
38+
len(missing),
39+
len(expected),
40+
preview,
41+
suffix,
42+
)
43+
model.ctc_decoder = None
44+
model.ctc = None
45+
model.ctc_tokenizer = None
46+
model.blank_id = None
47+
return missing

funasr/models/fun_asr_nano/inference_vllm.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import torch
3434
import torch.nn as nn
3535

36+
from .checkpoint_utils import disable_incomplete_ctc, normalize_checkpoint_state
37+
3638
logger = logging.getLogger(__name__)
3739

3840
dtype_map = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}
@@ -343,20 +345,7 @@ def _load_audio_components(self, model_dir: str, **kwargs):
343345

344346
# CTC decoder
345347
if self.ctc_decoder is not None:
346-
ctc_dec_state = {
347-
k[len("ctc_decoder."):]: v
348-
for k, v in state_dict.items()
349-
if k.startswith("ctc_decoder.")
350-
}
351-
if ctc_dec_state:
352-
self.ctc_decoder.load_state_dict(ctc_dec_state, strict=False)
353-
ctc_state = {
354-
k[len("ctc."):]: v
355-
for k, v in state_dict.items()
356-
if k.startswith("ctc.") and not k.startswith("ctc_decoder.")
357-
}
358-
if ctc_state:
359-
self.ctc.load_state_dict(ctc_state, strict=False)
348+
self._load_ctc_weights(state_dict)
360349

361350
# Move to device
362351
self.audio_encoder = self.audio_encoder.to(self.device, dtype=torch.float32)
@@ -365,6 +354,35 @@ def _load_audio_components(self, model_dir: str, **kwargs):
365354
self.ctc_decoder = self.ctc_decoder.to(self.device, dtype=torch.float32)
366355
self.ctc = self.ctc.to(self.device, dtype=torch.float32)
367356

357+
def _load_ctc_weights(self, state_dict):
358+
"""Load timestamp modules and disable them when the checkpoint is incomplete."""
359+
if self.ctc_decoder is None or self.ctc is None:
360+
return
361+
362+
state_dict = normalize_checkpoint_state(state_dict)
363+
364+
def compatible_state(module, prefix):
365+
expected = module.state_dict()
366+
return {
367+
key[len(prefix) :]: value
368+
for key, value in state_dict.items()
369+
if key.startswith(prefix)
370+
and key[len(prefix) :] in expected
371+
and value.size() == expected[key[len(prefix) :]].size()
372+
}
373+
374+
ctc_dec_state = compatible_state(self.ctc_decoder, "ctc_decoder.")
375+
if ctc_dec_state:
376+
self.ctc_decoder.load_state_dict(ctc_dec_state, strict=False)
377+
378+
ctc_state = compatible_state(self.ctc, "ctc.")
379+
if ctc_state:
380+
self.ctc.load_state_dict(ctc_state, strict=False)
381+
382+
loaded_keys = {f"ctc_decoder.{key}" for key in ctc_dec_state}
383+
loaded_keys.update(f"ctc.{key}" for key in ctc_state)
384+
disable_incomplete_ctc(self, loaded_keys, log=logger)
385+
368386
def _load_embedding_layer(self, model_dir: str):
369387
"""Load the LLM embedding layer for text token embedding computation."""
370388
model_pt = os.path.join(model_dir, "model.pt")

funasr/models/fun_asr_nano/model.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
AutoModelForCausalLM = None
2323

2424
from .ctc import CTC
25+
from .checkpoint_utils import disable_incomplete_ctc, normalize_checkpoint_state
2526
from .device_utils import resolve_autocast_device_type
2627
from .tools.utils import forced_align
2728

@@ -145,6 +146,7 @@ def __init__(
145146

146147
# ctc decoder
147148
self.ctc_decoder = None
149+
self._externally_loaded_ctc_keys = set()
148150
# TODO: fix table name
149151
ctc_decoder_class = tables.adaptor_classes.get(kwargs.get("ctc_decoder", None))
150152
if ctc_decoder_class is not None:
@@ -171,8 +173,15 @@ def __init__(
171173
self.ctc_decoder = ctc_decoder_class(**ctc_decoder_conf)
172174
init_param_path = ctc_decoder_conf.get("init_param_path", None)
173175
if init_param_path is not None:
174-
src_state = torch.load(init_param_path, map_location="cpu")
176+
src_state = normalize_checkpoint_state(
177+
torch.load(init_param_path, map_location="cpu")
178+
)
175179
flag = self.ctc_decoder.load_state_dict(src_state, strict=False)
180+
self._externally_loaded_ctc_keys.update(
181+
f"ctc_decoder.{key}"
182+
for key in self.ctc_decoder.state_dict()
183+
if key in src_state
184+
)
176185
logging.info(f"Loading ctc_decoder ckpt: {init_param_path}, status: {flag}")
177186
freeze = ctc_decoder_conf.get("freeze", False)
178187
if freeze:
@@ -196,6 +205,11 @@ def __init__(
196205
rank = int(os.environ.get("RANK", 0))
197206
logging.info(f"rank: {rank}, model is builded.")
198207

208+
def on_pretrained_model_loaded(self, loaded_keys):
209+
"""Fail closed when a checkpoint configures CTC without trained weights."""
210+
loaded_keys = set(loaded_keys).union(getattr(self, "_externally_loaded_ctc_keys", ()))
211+
disable_incomplete_ctc(self, loaded_keys, log=logging)
212+
199213
def forward(
200214
self,
201215
speech: torch.Tensor = None,

funasr/train_utils/load_pretrained_model.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def load_pretrained_model(
4545
src_state = src_state["state_dict"] if "state_dict" in src_state else src_state
4646
src_state = src_state["model_state_dict"] if "model_state_dict" in src_state else src_state
4747
src_state = src_state["model"] if "model" in src_state else src_state
48+
loaded_keys = set()
4849

4950
if isinstance(scope_map, str):
5051
scope_map = scope_map.split(",")
@@ -96,8 +97,18 @@ def load_pretrained_model(
9697
)
9798
else:
9899
dst_state[k] = src_state[k_src]
100+
loaded_keys.add(k)
99101
else:
100102
print(f"Warning, miss key in ckpt: {k}, {path}")
101103

102104
flag = obj.load_state_dict(dst_state, strict=True)
105+
on_loaded = getattr(obj, "on_pretrained_model_loaded", None)
106+
if not callable(on_loaded) and hasattr(obj, "module"):
107+
on_loaded = getattr(obj.module, "on_pretrained_model_loaded", None)
108+
if callable(on_loaded):
109+
loaded_keys = {
110+
key[len("module.") :] if key.startswith("module.") else key for key in loaded_keys
111+
}
112+
if callable(on_loaded):
113+
on_loaded(frozenset(loaded_keys))
103114
logging.info(f"Loading ckpt: {path}, status: {flag}")

0 commit comments

Comments
 (0)