Skip to content

Fix/ser wav2vec2 regression head#511

Merged
satra merged 21 commits into
alphafrom
fix/ser-wav2vec2-regression-head
May 13, 2026
Merged

Fix/ser wav2vec2 regression head#511
satra merged 21 commits into
alphafrom
fix/ser-wav2vec2-regression-head

Conversation

@wilke0818

@wilke0818 wilke0818 commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes continuous SER, at least for a subset of models by using a custom class that matches the expected class

Related Issue(s)

None

Motivation and Context

Use of the continuous SER by collaborators

How Has This Been Tested?

Manually

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • My code follows the code style of this project.

Version

Published prerelease version: 1.3.1-alpha.30

Changelog

🐛 Bug Fix

Authors: 3

satra and others added 3 commits May 1, 2026 11:48
Merge alpha: YAMNet backend, auto-resampling, pdoc cleanup
Promote scalene profiling wrapper to main (alpha→main)
@wilke0818
wilke0818 requested a review from satra May 8, 2026 21:41

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces a custom backend for models using the Wav2Vec2ForSpeechClassification architecture to ensure regression heads are loaded correctly rather than randomly initialized. Key additions include a custom regression head and model class, along with logic to detect and route these specific models during inference. Review feedback suggests replacing broad exception handling with more specific types and refactoring duplicated configuration loading logic into a shared helper function.

Comment thread src/senselab/audio/tasks/classification/speech_emotion_recognition/api.py Outdated

@satra satra 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.

Thanks for tracking this down — the head-structure mismatch diagnosis is solid and the new _Wav2Vec2EmotionModel reproduces the original audeering class faithfully. A few items worth addressing before merge:

CI: cpu-tests (3.12) is failing on this commit (job 75099350753). The PR description ticks "All new and existing tests passed" — please investigate; test_speech_emotion_recognition_continuous exercises this exact path now.

Correctness (see inline):

  1. Silent sampling-rate hazard — Wav2Vec2Processor does not resample.
  2. id2label[i] can KeyError in the raw-dict fallback path because HF JSON keys are strings.

Test coverage: the existing continuous-SER test only checks label names, which would have passed before this fix too (random head still produced correct labels with bogus scores). Worth adding:

  • assert set(labels) == {"arousal", "valence", "dominance"}
  • scores in [0, 1]
  • regression guard that not all scores ≈ 0.33 (any(abs(s - 0.33) > 0.05 for s in scores))

Minor cleanup (see inline): redundant _AC alias; Tuple import unused if switched to lowercase tuple; class factory rebuilds on every cache miss.

Otherwise looks good — once the CI failure is understood and the resample/id2label edges are handled, happy to see this land.


Generated by Claude Code

Comment thread src/senselab/audio/tasks/classification/speech_emotion_recognition/api.py Outdated
Comment thread src/senselab/audio/tasks/classification/speech_emotion_recognition/api.py Outdated
Comment thread src/senselab/audio/tasks/classification/speech_emotion_recognition/api.py Outdated
Comment thread src/senselab/audio/tasks/classification/speech_emotion_recognition/api.py Outdated
claude and others added 6 commits May 9, 2026 15:06
- Auto-resample audio to the processor's expected sampling rate to
  avoid silent correctness bugs when callers pass non-16 kHz input.
- Coerce id2label keys to int (HF stores them as strings in JSON) and
  fall back to class_{i} so the raw-config path doesn't KeyError.
- Memoize the dynamically built Wav2Vec2 emotion model class so
  isinstance checks stay stable across calls.
- Drop redundant `AutoConfig as _AC` alias and unused `Tuple` import.
- Strengthen the continuous-SER test with score-range and regression
  assertions that would catch the random-head bug this PR fixes.
Address Gemini review on PR #513: the previous slice-only logic could
produce a labels list shorter than the model output if num_labels in
the config doesn't match the actual head size. Pad with class_{i} so
AudioClassificationResult always sees matching lengths.
The 'any(abs(s - 0.33) > 0.05 for s in scores)' check would fail if
the test fixture happened to produce audeering outputs all in
[0.28, 0.38]. Keep only the label-set and [0, 1] range checks, which
are robust to any fixture content.
Three follow-on issues surfaced when running the new continuous-SER backend
end-to-end against audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim:

1. AutoConfig fallback raised TypeError because config.json's own
   "model_type" key collided with the positional arg in
   AutoConfig.for_model("wav2vec2", ...). Pop it before splat.

2. The same checkpoints ship vocab_size: null, which trips
   huggingface_hub>=1.0 strict-dataclass validators when Wav2Vec2Config is
   instantiated. The regression head doesn't use vocab_size, so drop the
   key and let the dataclass default apply.

3. _Wav2Vec2EmotionModel called self.init_weights(), but
   transformers>=5.0's from_pretrained finalizer reads
   self.all_tied_weights_keys, which is populated by post_init. Switch to
   post_init() so loading succeeds.

Additionally swap Wav2Vec2Processor.from_pretrained for
Wav2Vec2FeatureExtractor.from_pretrained, since the processor path
re-invokes AutoConfig.from_pretrained for the tokenizer (re-tripping the
vocab_size validator) and regression inference doesn't need a tokenizer.

Mypy cleanups along the way: cast config to Wav2Vec2Config at the
Wav2Vec2Model construction site, and route feature_extractor.sampling_rate
through getattr to drop the attr-defined warning on Wav2Vec2Processor.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
Fix Wav2Vec2 emotion model initialization and sampling rate handling
@satra
satra temporarily deployed to docs-preview May 9, 2026 17:22 — with GitHub Actions Inactive
@satra
satra temporarily deployed to docs-preview May 9, 2026 20:34 — with GitHub Actions Inactive
Wires the audeering Wav2Vec2ForSpeechClassification fix from PR #513 into
the discrete-SER path so the ehcalabres RAVDESS checkpoint stops emitting
~1/8-uniform softmax outputs:

* Generalize the custom emotion classifier head to support both final-layer
  attribute names actually shipped in the wild: ``classifier.out_proj.*``
  (audeering MSP-Dim) and ``classifier.output.*`` (ehcalabres). Build the
  matching nn.Module per layout and cache by name.

* Detect the head layout cheaply via the checkpoint's safetensors index or
  bin shard manifest (no full weight download); fall back to a small
  registry for single-bin checkpoints like ehcalabres.

* Dispatcher now routes any Wav2Vec2 emotion checkpoint with a 2-layer
  dense+linear head through ``_classify_wav2vec2_speech_cls_ser``,
  applying softmax for discrete and leaving raw logits for continuous.

* Discrete SER test asserts max(scores) > 0.2 to detect random-head
  regressions, plus that scores form a softmax distribution.

Wraps every SpeechBrain ``from_hparams`` call in
``speechbrain_loading_cwd(savedir)`` so CWD-relative ``save_path`` lobes
inside hyperparams.yaml (e.g. ``save_path: wav2vec2_checkpoints`` on
``speechbrain/emotion-recognition-wav2vec2-IEMOCAP``) resolve under
``{HF_HOME}/senselab_cache/speechbrain/<safe_key>/`` instead of dropping
700+MB into the user's working tree.

Verified end-to-end against two HAPPY recordings: ehcalabres now returns
``happy: 0.49`` (vs. ~0.13 uniform before), and CWD stays clean after a
full SER pipeline run.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
@satra
satra temporarily deployed to docs-preview May 9, 2026 20:39 — with GitHub Actions Inactive
…or, KeyError)

Addresses gemini-code-assist's review on PR #511 about broad-except.
The actual exception observed in the audeering MSP-Dim case is
huggingface_hub.errors.StrictDataclassError (parent of
StrictDataclassFieldValidationError, raised on vocab_size: null with
huggingface_hub>=1.0). Standard parsing failures fall under ValueError /
TypeError / KeyError. OSError is intentionally excluded — that means the
network or filesystem is broken, and the fallback path needs the same
network hop, so it can't recover.

The tuple is constructed with a try/except ImportError to stay compatible
with huggingface_hub<1.0 (where StrictDataclassError doesn't exist).

Verified by re-running test_speech_emotion_recognition_continuous against
audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim — the
StrictDataclassFieldValidationError is correctly caught by the narrowed
tuple and the raw-config-dict fallback runs as before.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
@satra
satra temporarily deployed to docs-preview May 9, 2026 21:17 — with GitHub Actions Inactive
…ation plan

Phase 1 of specs/20260509-204500-ser-silent-failure-guards/plan.md.

The custom Wav2Vec2 emotion-head path in `_classify_wav2vec2_speech_cls_ser`
trusts the dispatcher peek to have correctly matched the checkpoint's head
layout. When that assumption is wrong (new encoder family, unrecognized
final-layer attribute name, or single-bin checkpoint not in the registry)
transformers silently random-initializes the head and inference returns
~uniform softmax — the exact bug PR #511 is fixing, just one routing branch
upstream.

Convert that to a loud `RuntimeError`. After `from_pretrained` returns we
inspect `loading_info` for any `classifier.*` entry in `missing_keys`
(weights absent, would be randomly initialized) **or** `mismatched_keys`
(weights present but wrong shape — equally a silent-random failure that
the original missing-keys check would miss; flagged by architect review).
We also assert the final layer's `out_features` equals
`config.num_labels` / `len(id2label)`, catching the case where head and
config disagree on the number of classes.

Adds a parametrised unit test that mocks `EmotionModel.from_pretrained`
to return each broken-load shape and asserts the diagnostic raises with
a matching message.

Also commits the full remediation plan (specs/.../plan.md) enumerating
Phases 2-7 (standard-pipeline missing-key warning, encoder-family
generalization for HuBERT/WavLM, head-registry richening, problem_type
based discrete/continuous detection, resampling in the standard pipeline,
diagnostics CLI). Plan was self-audited and reviewed by the Plan agent
before this commit; its recommendations are folded in (mismatched_keys
coverage promoted into Phase 1, dynamic HeadSpec cut from Phase 4 as
premature, encoder-attribute-name registry promoted from a Phase 3
footnote to a load-bearing task).

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
claude and others added 7 commits May 9, 2026 22:40
Implements the remaining phases from
specs/20260509-204500-ser-silent-failure-guards/plan.md, layered on top
of Phase 1's loud-fail guard:

- Phase 4 (head registry): introduce ``_HeadEntry`` dataclass holding
  ``final_layer`` / ``activation`` / ``dropout_field``; promote
  ``_KNOWN_WAV2VEC2_EMOTION_HEADS`` (str→str) to ``_KNOWN_HEAD_LAYOUTS``
  (str→_HeadEntry). Generalize ``_make_wav2vec2_regression_head_class``
  to ``_make_regression_head_class(head: _HeadEntry)`` with a small
  ``_ACTIVATIONS`` table. Cache key now includes activation/dropout_field
  so future entries don't collide.

- Phase 3 (encoder-family generalization): ``_BASE_REGISTRY`` maps
  ``config.model_type`` → ``(PreTrainedModel base name, encoder attr
  name)`` for wav2vec2/hubert/wavlm/wav2vec2-bert. Resolved lazily so
  imports only fire when the custom-head path runs. ``_emotion_head_kind``
  now returns ``(model_type, _HeadEntry)`` tuples; ``_make_emotion_model_class``
  threads the right encoder class and attribute name. The Phase-1 guard
  catches mis-registered families: any wrong attr-name will surface as
  "every classifier.* key missing" with a clear ``RuntimeError``.

- Phase 5 (problem_type detection): ``_resolve_apply_softmax`` prefers
  ``config.problem_type`` over the keyword-based label heuristic. A
  regression head with non-AVD axes (e.g. ``["energy","pleasantness"]``)
  no longer gets softmax misapplied. Falls back to ``_get_ser_type`` for
  legacy checkpoints that omit ``problem_type``.

- Phase 2 (standard-pipeline missing-key warning):
  ``_get_hf_audio_classification_pipeline`` now loads the model
  explicitly via ``AutoModelForAudioClassification.from_pretrained(...,
  output_loading_info=True)`` and threads the loaded instance into
  ``pipeline(model=..., feature_extractor=...)``. ``_check_head_loaded_cleanly``
  warns on suspect missing/mismatched head keys
  (``classifier.``/``head.``/``score.``/``out_proj.``/``projector.``).
  ``SENSELAB_STRICT_HEAD_LOAD=1`` promotes the warning to a hard error.

- Phase 6 (resampling in standard pipeline): verified already implemented
  at ``classify_audios_with_transformers`` lines 141-142; no code change
  required. Plan updated to reflect the discovery.

- Phase 7 (docs + diagnostics CLI):
  - ``doc.md`` enumerates the three dispatch paths (custom in-process
    head / subprocess venv / standard pipeline) and points at the CLI.
  - New ``python -m
    senselab.audio.tasks.classification.speech_emotion_recognition
    --probe <repo_id>`` reports model_type, architectures, dispatch
    decision, head source, and emits advisories. Pure config / manifest
    inspection — no weights loaded.

Backwards-compat shims kept in place:
- ``_make_wav2vec2_emotion_model_class(final_layer)`` (delegates to
  ``_make_emotion_model_class("wav2vec2", _HeadEntry(final_layer=...))``)
- ``_wav2vec2_emotion_head_kind`` (returns just the final-layer name from
  the new ``_emotion_head_kind`` tuple).
- ``final_layer=`` kwarg on ``_classify_wav2vec2_speech_cls_ser``
  short-circuits to ``_HeadEntry(final_layer=...)``.

Verified locally: all 8 classification tests pass (1 GPU-skip), audeering
happy path passes, ehcalabres CLI probe correctly identifies its head
from ``_KNOWN_HEAD_LAYOUTS`` registry, audeering CLI probe peeks the
manifest, superb correctly routes to the standard pipeline with an
advisory.

Test coverage gaps (deferred per plan):
- Phase 2 unit test for ``_check_head_loaded_cleanly`` (covered
  end-to-end by audeering/ehcalabres tests for now).
- Phase 5 unit test for ``_resolve_apply_softmax`` (no fixture model
  with problem_type=regression + confounding labels).
- Phase 4 numerical-regression test on existing checkpoints.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
Two small fixes from the second-pass architect review:

1. Make ``_BASE_REGISTRY`` a 3-tuple ``(base_pretrained_cls_name,
   encoder_model_cls_name, encoder_attr_name)`` instead of deriving the
   encoder class name via ``base_name.replace("PreTrainedModel", "Model")``.
   The string-mutation worked for current Wav2Vec2/HuBERT/WavLM/W2V-BERT
   but would silently break the day a family ships an encoder with a
   different naming convention. Explicit > clever.

   Updated both indexers (``_emotion_head_kind`` in api.py and the
   probe CLI's status print) to read attr name from index ``[2]``.

2. Probe CLI: when ``AutoConfig.from_pretrained`` fails (e.g. audeering's
   ``vocab_size: null`` tripping the strict validator), make the
   diagnostic note explicit that the dispatcher's raw-config-dict
   fallback handles this — the routing decision and advisories that
   follow still work correctly because ``getattr(None, ...)`` short-
   circuits the ``config.model_type`` checks.

Verified: existing guard tests pass; ``uv run python -m
senselab.audio.tasks.classification.speech_emotion_recognition --probe
audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim`` still produces a
correct routing report end-to-end.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
CI pre-commit hook runs mypy with --extra-checks which flagged the
inferred type for ``model = HFModel(...)`` in __main__.py. Add the
explicit annotation. cpu-tests already pass; this is the only blocker.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
…2 unit tests

The strict reviewer identified 5 critical defects against the user's
"zero tolerance for silent failures" criterion. All addressed in this
commit:

D1. ``wav2vec2-bert`` removed from ``_BASE_REGISTRY``.
    The family consumes log-mel ``input_features`` (via
    ``SeamlessM4TFeatureExtractor``) rather than raw ``input_values``.
    The loader hardcodes the latter and accesses ``inputs["input_values"]``
    so any wav2vec2-bert checkpoint would have produced ``KeyError`` at
    best, silent garbage at worst. Add a per-family preprocessing
    adapter before re-introducing.

D2. Phase-1 guard now covers encoder-key misses.
    Previous filter was ``classifier.*`` only, which meant a wrong
    ``_BASE_REGISTRY`` entry (wrong encoder attribute name) would leave
    every encoder weight missing yet pass the guard — silent garbage
    output. The guard now treats any missing key under either
    ``classifier.`` or ``{encoder_attr}.`` as suspect, with a small
    whitelist of HF post_init buffers (``masked_spec_embed``).

D3. ``_resolve_apply_softmax`` peeks ``architectures`` on
    ``problem_type``-absent checkpoints. Any model declaring
    ``Wav2Vec2ForSpeechClassification`` (audeering signature) is treated
    as regression. Reduces the silent-corruption window for regression
    heads with English-emotion-word labels.

D4. ``_peek_head_final_layer`` raises when ``classifier.dense.weight``
    is present but no recognized final-layer attribute matches.
    Previously returned None and let the standard pipeline silently
    random-initialize the head. The error message lists the actual
    classifier keys so a maintainer can extend ``_FINAL_LAYER_NAMES``
    or ``_KNOWN_HEAD_LAYOUTS``.

D5. ``SENSELAB_STRICT_HEAD_LOAD`` default inverted to ``=1`` (raise).
    Warn-only meant downstream consumers like
    ``classify_audios`` windowed segmentation kept aggregating
    corrupted scores. Set ``SENSELAB_STRICT_HEAD_LOAD=0`` to demote to
    a warning if you intentionally want a partial-head load (e.g.
    fine-tuning warm-start).

T203. Three unit tests for ``_check_head_loaded_cleanly``: parametrised
    raise on suspect missing/mismatched-shape keys (default strict),
    warn-only mode under ``SENSELAB_STRICT_HEAD_LOAD=0``, and silent
    pass on clean loads (encoder-buffer misses don't trip).

Plan checkboxes T203 / T304 / T502 promoted from deferred-shortcut to
actually-shipped. Audeering happy-path test still passes; mypy clean
with ``--extra-checks``.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
Reviewer round 3 verified D1-D5 + T203 fixes are functionally correct,
but flagged three pieces of user-facing text that still contradict the
new behavior — themselves a silent-assumption violation per the user's
"no silent failures and assumptions beneath those" criterion:

* ``doc.md`` listed ``wav2vec2-bert`` as a supported encoder for the
  custom-head path (it isn't; D1 dropped it from ``_BASE_REGISTRY``)
  and described ``SENSELAB_STRICT_HEAD_LOAD=1`` as a way to "promote
  the warning to a RuntimeError" — but ``=1`` is now the default
  (D5). A user who reads the doc, doesn't set the env var, and
  expects warn-mode would be surprised by a hard fail.

* ``__main__.py`` probe CLI advisory had the same inverted message
  about the env var.

* ``api.py:503`` dispatcher comment + ``api.py:144`` encoder-attribute
  comment still mentioned ``wav2vec2-bert``.

This commit:

1. Removes ``wav2vec2-bert`` from the doc's supported-encoder list,
   adds an explicit "intentionally not supported" sentence with the
   ``SeamlessM4TFeatureExtractor`` reason, so users know why and what
   it would take to add.
2. Inverts the env-var description in both the doc and the CLI to
   match the strict-by-default semantics.
3. Drops stale ``wav2vec2-bert`` mentions from the api.py comments.

No code-path or behavior change. Verified clean: ruff, ruff format,
mypy --extra-checks all pass.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
When the strict reviewer's second pass forced T203 and T502 from
deferred-shortcut to actually-shipped, I appended the new ``[x]`` text
and kept the original ``[ ]`` lines tagged "legacy text, kept for
traceability". On re-read those duplicates just made the plan look
like there were unfinished items when there weren't. Drop the
duplicates; the ``[x]`` entries are sufficient.

Remaining ``[ ]`` items after cleanup:
  T305 — attention-mask shim for batched HuBERT/WavLM (don't optimize for unknowns)
  T403 — numerical regression test against fixed scores (Phase-1 guard + bounded-score covers)
  T602 — sampling-rate test (Phase 6 was a no-change phase, end-to-end coverage exists)

All three are honest deferrals, not shortcuts.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
Implements all phases of the SER silent-failure-guards plan, with two strict reviewer passes folding in 5 critical defects + 3 stale-doc fixes.

Phases:
- 1: Loud-fail post-load assertion in the custom-head path (covers classifier and encoder keys).
- 2: Standard-pipeline head-load guard (RuntimeError by default; SENSELAB_STRICT_HEAD_LOAD=0 to demote).
- 3: Encoder-family generalization (wav2vec2/hubert/wavlm; wav2vec2-bert deferred).
- 4: HeadEntry registry refactor.
- 5: problem_type detection with architecture peek fallback.
- 6: Sampling-rate resampling (verified pre-existing).
- 7: Doc updates + diagnostic --probe CLI.

See PR #514 description for the full reviewer history and explicit caveats.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
@satra
satra temporarily deployed to docs-preview May 10, 2026 05:54 — with GitHub Actions Inactive
@wilke0818

Copy link
Copy Markdown
Collaborator Author

seems reasonable. the main issues that were pointed out by claude:

  1. os.chdir is process-global. speechbrain_loading_cwd changes the process working directory, which will silently affect any other thread running concurrently during that
    block. For a single-threaded use case this is fine, but if someone ever calls from_hparams from multiple threads simultaneously (e.g. loading two SpeechBrain models in
    parallel), they'll get CWD races. A comment warning about this would be prudent, or alternatively keeping it to the known-bad CWD paths only.
  2. The _emotion_head_kind call happens twice in the happy path — once in classify_emotions_from_speech and once implicitly when building the model key. Each call fetches the
    config from HuggingFace Hub. For the two known checkpoints this is fast (config-only), but it's still two round-trips on every inference call until the result is cached. The
    function result could be memoized.
  3. The _resolve_apply_softmax function also calls AutoConfig.from_pretrained, meaning a third config fetch per inference call (again until caching kicks in). This is a minor
    inefficiency but worth noting.
  4. The test for the existing discrete model (test_speech_emotion_recognition_discrete) still requires a GPU (gpu_device fixture) and no test covers the new HuBERT/WavLM
    registry entries in practice. The Phase 1 unit tests are good but they mock everything — there's no integration-level confidence that the HuBERT/WavLM entries in
    _BASE_REGISTRY actually work on real checkpoints.
  5. The plan doc (specs/plan.md) being committed to the repo is a somewhat unusual convention. It's useful for posterity, but embedding it in the working tree means it'll
    accumulate and diverge from reality over time. A reference to a GitHub issue or discussion thread would age better.

satra commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the careful review — all five points addressed (or, in #5's case, defended) in #515 which stacks on this branch:

  1. CWD racespeechbrain_loading_cwd now serializes via a module-level threading.Lock. Docstring documents the trade-off: parallel SpeechBrain init across threads now blocks rather than races; processes are the escape hatch.
  2. & 3. Redundant AutoConfig.from_pretrained calls — introduced _load_config_cached(model) returning (typed_config_or_None, raw_dict_or_None) memoized on (path, revision). The four call sites (_get_ser_type, _emotion_head_kind, _resolve_apply_softmax, _classify_wav2vec2_speech_cls_ser) now share one round-trip. Added test_load_config_cached_round_trips_once to lock that in.
  3. No HuBERT/WavLM integration confidence — partial fix: three parametrised unit tests verify _resolve_base("wav2vec2"|"hubert"|"wavlm") resolve to real transformers classes (catches _BASE_REGISTRY typos at test-collection time). Real-checkpoint integration tests are still future work — happy to file an issue if you want to track.
  4. Plan doc in specs/ — skipped for consistency with PR Fix Wav2Vec2 emotion model initialization and sampling rate handling #513's pattern (specs/20260506-.../tasks.md). Genuinely open to changing it if maintainers prefer issue-based docs; let me know.

Generated by Claude Code

… lock + registry smoke tests (#515)

Stacks on top of PR #511 to address @wilke0818's review.

Changes:
1. **CWD race fix**: `speechbrain_loading_cwd` now serializes via a
   module-level `threading.Lock` so concurrent threads can't race on the
   process-global `os.chdir`. Docstring documents the trade-off (use
   processes, not threads, for parallel SpeechBrain init).

2. **AutoConfig deduplication**: introduces `_load_config_cached(model)`
   returning `(typed_config_or_None, raw_dict_or_None)`, memoized
   per-(path, revision). The four call sites (`_get_ser_type`,
   `_emotion_head_kind`, `_resolve_apply_softmax`,
   `_classify_wav2vec2_speech_cls_ser`) now share one network round-trip
   instead of four. Type annotated with `PretrainedConfig`.

3. **Per-key locking** (not a single global lock): mirrors the strategy
   used by `dependencies.ensure_hf_model` (`_HeartbeatLock` per
   `(repo_id, revision)`). Two threads loading the *same* model dedupe
   to one round-trip; two threads loading *different* models proceed in
   parallel. In-memory `threading.Lock` per key here, since the memo is
   per-process (cross-process is already handled by HF_HOME cache and
   `ensure_hf_model`).

4. **`_BASE_REGISTRY` smoke tests**: three parametrised unit tests verify
   `_resolve_base("wav2vec2"|"hubert"|"wavlm")` resolve to real
   transformers classes — catches typos at test-collection time without
   downloading model weights.

5. **Test isolation fix**: autouse fixture clears the module-level caches
   (`_config_memo`, `_config_memo_locks`, `_wav2vec2_emotion_models`)
   between tests. Previously the parametrised
   `test_wav2vec2_speech_cls_ser_raises_on_random_head` and
   `_raises_on_shape_mismatch` would silently serve a cached MagicMock
   from a prior test's monkeypatch instead of re-invoking their own;
   tests only passed because the mocks happened to agree.

Verified locally: ruff, format, mypy --extra-checks clean. 18 fast tests
pass including 4 new ones. CI: ✅ cpu-tests, pre-commit, build-and-deploy
all green on `be77c5d`.

https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66

satra commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Follow-up #515 just landed on this branch (squash-merged as 834be0c). The diff on this PR now reflects all five fixes from your review:

# Resolution
1 (CWD race) speechbrain_loading_cwd serializes via module-level lock; docstring warns to use processes for parallel SpeechBrain init
2 + 3 (redundant AutoConfig.from_pretrained) _load_config_cached(model) memoizes typed config + raw dict per (path, revision). Per-key locking (one threading.Lock per cached entry) so concurrent loads of different models don't serialize — mirrors dependencies.ensure_hf_model's _HeartbeatLock strategy. The 4 call sites now share one network round-trip
4 (no HuBERT/WavLM tests) 3 parametrised _resolve_base smoke tests catch _BASE_REGISTRY typos at test-collection time without downloading checkpoints. Real-checkpoint integration tests still future work — happy to file a separate issue if you want to track
5 (plan doc convention) Kept (consistent with #513); open to changing if you'd prefer issue-based docs

CI on the latest commit is green; ready when you are.


Generated by Claude Code

@wilke0818

Copy link
Copy Markdown
Collaborator Author

seems good

@satra satra added the ec2-gpu-test Trigger EC2 GPU tests label May 12, 2026
@satra

satra commented May 12, 2026

Copy link
Copy Markdown
Collaborator

adding the gpu tests also to check the venv stuff (i think the image does have cuda 12.8 so this should still pass there).

@satra
satra merged commit 1443cdf into alpha May 13, 2026
16 checks passed
@satra
satra deleted the fix/ser-wav2vec2-regression-head branch May 13, 2026 00:43
github-actions Bot added a commit that referenced this pull request May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ec2-gpu-test Trigger EC2 GPU tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants