Skip to content

feat: add local Qwen3-Omni API and fine-tune regression - #318

Merged
glennko merged 8 commits into
mainfrom
glenn/qwen3omni
Sep 8, 2026
Merged

feat: add local Qwen3-Omni API and fine-tune regression#318
glennko merged 8 commits into
mainfrom
glenn/qwen3omni

Conversation

@glennko

@glennko glennko commented Feb 5, 2026

Copy link
Copy Markdown
Member
  • swap Qwen3-Omni dataset generation to load Hugging Face checkpoints locally and wire up tests/ docs
  • ensure AGENTS.md/CLAUDE.md stay untracked
  • add a lightweight GPT-2 fine-tuning test that asserts weights actually change

Summary

Checklist

  • Tested
  • Documented

Additional Information

* swap Qwen3-Omni dataset generation to load Hugging Face checkpoints locally and wire up tests/
  docs
  * ensure AGENTS.md/CLAUDE.md stay untracked
  * add a lightweight GPT-2 fine-tuning test that asserts weights actually change
Comment thread src/xturing/model_apis/qwen.py Outdated
self.tokenizer = AutoTokenizer.from_pretrained(
model_name_or_path, trust_remote_code=True, **tokenizer_kwargs
)
self.model = AutoModelForCausalLM.from_pretrained(

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.

It doesn't look correct. I don't think AutoModelForCausalLM is supported for this model. Just try AutoModelForMultimodalLM

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've made the suggested changes.

chore: apply autoflake/isort fixes

style: apply black formatting
# Conflicts:
#	README.md
#	examples/README.md
#	tests/xturing/models/test_gpt2_model.py
`AutoModelForMultimodalLM` was added in transformers 5.0.0. Importing it at
module scope made `xturing.model_apis` unimportable on every 4.x release, and
that package is pulled in by `xturing.datasets.instruction_dataset`, so the
documented `from xturing.datasets import InstructionDataset` quickstart raised
ImportError.

Guard the import the way ClaudeTextGenerationAPI guards `anthropic`, but catch
ImportError rather than ModuleNotFoundError: transformers is installed, only
the symbol is absent. Construction now raises an actionable error naming the
required version.

Tests drop `raising=False` so a missing or renamed symbol fails loudly, and
cover both the graceful import and the error path.
@glennko

glennko commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Review findings

Re-reviewed against current main. The approval here predates ~7 months of drift, so flagging what's changed and what I think blocks.

The rebase is a non-issue. Merge base is fb16cc2; every commit main has gained since is a Dependabot bump to docs/package-lock.json plus the lockfile refresh in #342. Zero overlap with this PR's changed files — BEHIND here is pure branch-protection bookkeeping, not conceptual drift. CI gates pass on the head: black --check clean, isort clean, scripts/check_docs_contracts.py passes.

Blocking

1. The API is unreachable on a supported install. qwen.py needs AutoModelForMultimodalLM, which requires transformers>=5.0.0, but pyproject.toml:46 still pins >=4.36.0 and this PR doesn't bump it. Confirmed against the resolved 4.36.2: hasattr(transformers, "AutoModelForMultimodalLM") is False. So every construction of Qwen3OmniTextGenerationAPI raises the ImportError at qwen.py:76 telling the user to upgrade — and that upgrade would likely break the rest of xTuring, which is written against 4.x. Meanwhile the README and docs/docs/advanced/generate.md advertise it as usable. Needs a pin bump, an optional extra, or a feature gate.

Also worth double-checking that AutoModelForMultimodalLM is the real symbol — Qwen3-Omni's own class is Qwen3OmniMoeForConditionalGeneration, and nothing in the repo can validate the name.

2. finish_reason is hardcoded, which silently corrupts the pipeline this exists to feed. qwen.py:129 always emits "stop", even when generation stopped on max_new_tokens. The self-instruct pipeline discards truncated completions by branching on finish_reason == "length" — see bootstrap_instructions.py:71 and prepare_for_finetuning.py:192,235. With this engine that branch is dead, so truncated mid-sentence generations get accepted into the generated dataset. Should compare completion token count against max_tokens and emit "length".

3. The "fine-tune regression" in the title never runs. Two independent gates: .github/workflows/ci.yml uses an explicit allowlist (tests/xturing/cli/test_api_server.py, tests/xturing/evaluation/test_runner.py) that includes neither new test file; and test_gpt2_model.py:24 gates on XTURING_RUN_TRAINING_TESTS == "1", which is set nowhere in the repo. As written it's documentation, not a regression guard.

The bigger miss is the 6 Qwen tests — they're mocked, CPU-only, fast, dependency-light, and would be a genuinely cheap CI addition. #343 adds a model-api-tests job covering tests/xturing/model_apis/, which would pick them up; worth landing that first and confirming these run under it.

4. Pad tokens aren't masked in the PEFT example. train_qwen3_omni_peft.py:229-233 clones input_ids into labels and masks only the prompt prefix; padding positions stay unmasked, so with padding=True and batch>1 the model computes loss over pad tokens. The sibling pad_batch() at line 62 does this correctly, so it's an inconsistency rather than a deliberate choice. Related: prompt_lens = prompt_inputs["attention_mask"].sum(dim=1) is only correct under right-padding.

Non-blocking

  • train_qwen3_omni_peft.py:243 passes tokenizer= to Trainer (deprecated 4.46, removed in 5.0.0 — the exact version this file needs) and torch_dtype= at line 141 (renamed to dtype=). Nothing catches it because the file is never imported.
  • README.md:181 gained a stray leading space inside a Python fence — copy-pasting now yields IndentationError.
  • The new "Running Tests" block claims @pytest.mark.slow/@pytest.mark.gpu are "used in this project". pytest.ini:4-7 registers them but no test uses either, so pytest -m "not slow" deselects nothing.
  • "Quick Contribution Guidelines" says PRs should target dev, contradicting CONTRIBUTING.md:44 ("target main") — and this PR itself.
  • docs/docs/advanced/generate.md links to Qwen2.5-Omni and uses model_name_or_path="Qwen/Qwen2.5-Omni" (not a valid repo id) in a Qwen3-Omni section.
  • .gitignore now ignores AGENTS.md/CLAUDE.md repo-wide, so nobody can ever commit those filenames. Note docs(contributing): drop dead AGENTS.md link #344 removes a CONTRIBUTING.md link to AGENTS.md.

Verified fine

generate_text correctly absorbs and discards the OpenAI-only kwargs (frequency_penalty, presence_penalty, logprobs, best_of) that all three self-instruct call sites pass, so they don't leak into model.generate(). The hard import torch in model_apis/qwen.py:5 does not break lightweight-tests — that job installs only pytest fastapi uvicorn httpx, and neither xturing.cli nor xturing.evaluation imports model_apis. Test quality is good: finetuning_config() returns the live args object, distilgpt2 defaults guarantee 2 optimizer steps fire, and weight_shift > 0.0 isn't flaky.

glennko added a commit that referenced this pull request Sep 3, 2026
* test(model_apis): make anthropic error construction version-independent

The helper passed response=/body= to every SDK error class, but anthropic's
signatures diverge (APIError takes request, RateLimitError takes response), so
the suite only worked against one SDK release. It failed outright on anthropic
1.2.0 with 'APIError.__init__() got an unexpected keyword argument response'.

This went unnoticed because tests/xturing/model_apis/ has never run in CI.

Subclass the SDK error with a permissive __init__ instead: the instance is
still caught by 'except error_cls' without depending on the signature.

* ci: add model-api-tests job covering tests/xturing/model_apis/

lightweight-tests installs only pytest/fastapi/uvicorn/httpx and runs two
files, so tests/xturing/model_apis/ has never executed in CI. The suite had
silently rotted: test_claude_api.py fails on current anthropic releases, and
a transformers-5-only import in the Qwen3-Omni wrapper reached four green
checks without being caught.

Add a separate job rather than widening lightweight-tests, so the existing
required check keeps its current runtime and scope.

These wrappers are pure Python -- no model weights are downloaded. torch is
installed from the CPU index because xturing.model_apis imports it
transitively and the default wheel pulls a large CUDA stack this job never
uses.

Verified locally against a clean venv (pytest + CPU torch + anthropic/cohere/
openai): 13 passed on main, and the Qwen3-Omni wrapper's 6 tests pass on the
#318 branch in the same environment.

---------

Co-authored-by: Glenn Ko <glennko@users.noreply.github.com>
@glennko

glennko commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Parking this — and a correction in your favour

Researched the transformers 5.x question properly, verified against the v5.16.1 source tree rather than from memory. Two of my earlier points need revising.

Correction: AutoModelForMultimodalLM is real

I said it was worth double-checking whether that symbol exists. It does. It's defined at models/auto/modeling_auto.py:2439 in v5.x, exported in __all__, and documented in model_doc/auto.md. It's new in v5.0.0 — grepping v4.56 and v4.57 returns zero hits, which is why it looked suspicious.

Your class mapping is also correct: MODEL_FOR_MULTIMODAL_LM_MAPPING_NAMES maps qwen3_omni_moeQwen3OmniMoeForConditionalGeneration, so AutoModelForMultimodalLM is the right auto-class, and it's the broadest any-to-any one. Qwen3-Omni support itself landed in v4.57.0, but only under AutoModelForTextToWaveform; the AutoModelForMultimodalLM route genuinely is v5-only. The approach in this PR is sound.

Confirmed: the pin bump is breaking, and worse than the README said

load_in_8bit= / load_in_4bit= were hard-removed from from_pretrained in v5.0.0 (PR #41287), not deprecated. Zero hits in modeling_utils.py at v5.16.1. The failure mode is a bare TypeError: __init__() got an unexpected keyword argument 'load_in_8bit' — no helpful migration error.

xturing passes them as bare kwargs in 7 places: engines/causal.py (50, 94, 251), llama_engine.py (60, 86), gptj_engine.py (46, 66). That part is a mechanical fix — BitsAndBytesConfig(load_in_8bit=True) passed as quantization_config=, and causal.py:83 already does exactly this for the woq path, so the pattern is in the codebase.

But the quantization kwargs are the small problem. Three pins in pyproject.toml sit below v5's hard floors:

declared v5.x requires
bitsandbytes==0.41.1 (exact pin) >= 0.46.1
accelerate==0.22.0 (exact pin) >= 1.1.0
torch >= 1.9.0 >= 2.5
requires-python = ">=3.7" >= 3.10

Plus a long tail that a LoRA library walks straight into: Trainer(tokenizer=) removed in favour of processing_class= (no **kwargs, so it's an immediate TypeError), TrainingArguments.warmup_ratio removed, safe_serialization=False removed, default dtype changed from fp32 to "auto", transformers.tokenization_utils moved (you import from it in 4 files — 3 collators plus llama_utils/llama.py), and use_auth_tokentoken (2 sites in quant_utils/). MoE experts were also refactored off nn.ModuleList, which broke PEFT adapters on MoE models — and Qwen3-Omni is MoE, so that's directly relevant here. PEFT floor for v5 is >= 0.19.1.

Why it's parked

This isn't a "fix four review comments" PR. Landing Qwen3-Omni means a transformers 5.x migration across the whole library — a dependency-floor bump that drops Python 3.7–3.9, plus quantization, Trainer, and tokenizer-import changes. That's its own epic, and it should not ride in on a feature PR.

The other three findings from my earlier review still stand and are worth fixing whenever this resumes — finish_reason hardcoded to "stop" at qwen.py:129 is the one with real consequences, since it silently feeds truncated generations into the self-instruct dataset.

#358 corrects the README note that #320 added, so main no longer points people at a 5.x upgrade that would break their INT8/INT4 install.

glennko added a commit that referenced this pull request Sep 3, 2026
#320 added an install note describing Qwen3-Omni's transformers>=5.0.0
requirement, but that feature is not merged (#318 is still open) and
pyproject.toml pins transformers>=4.36.0. The note documented the
requirements of something the package does not ship, and read as an
invitation to upgrade.

Verified against the transformers v5.16.1 source tree: load_in_8bit and
load_in_4bit were hard-removed from from_pretrained in v5.0.0 (PR
huggingface/transformers#41287), not deprecated. xturing passes them as
bare kwargs in 7 places across engines/causal.py, llama_engine.py and
gptj_engine.py, so a 5.x upgrade breaks the INT8/INT4 engines with a
bare TypeError.

Invert the note: state the supported floor, warn against 5.x, and point
at #318 for Qwen3-Omni rather than describing it as available.

Co-authored-by: Glenn Ko <glennko@users.noreply.github.com>
@glennko
glennko merged commit 9914066 into main Sep 8, 2026
6 checks passed
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.

2 participants