From 2e5d1fccc746067f1697a23121b5ff0fcfeccec4 Mon Sep 17 00:00:00 2001 From: BAR HAIM Date: Mon, 27 Jul 2026 12:24:23 +0000 Subject: [PATCH 1/5] tests: cover audio/adapter coexistence, chat-template marker, empty clips, clip ceiling Signed-off-by: BAR HAIM --- tests/composer/test_chat_template.py | 105 ++++++++++++++++++++- tests/unit/test_config.py | 30 ++++++ tests/vllm/test_audio_processor.py | 132 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 1 deletion(-) diff --git a/tests/composer/test_chat_template.py b/tests/composer/test_chat_template.py index 8aededf..9ffe599 100644 --- a/tests/composer/test_chat_template.py +++ b/tests/composer/test_chat_template.py @@ -27,9 +27,13 @@ from types import SimpleNamespace from unittest.mock import patch +import pytest from jinja2 import Environment -from granite_switch.composer.tokenizer_setup import configure_chat_template +from granite_switch.composer.tokenizer_setup import ( + configure_audio_chat_template, + configure_chat_template, +) _PATCH_TARGET = "granite_switch.composer.tokenizer_setup._decode_alora_invocation_text" @@ -531,3 +535,102 @@ def test_mixed_adapters_from_adapter_config(self): assert "<|answerability|>" not in result_none assert "<|context_relevance|>" not in result_none assert "<|summarization|>" not in result_none + + +# ════════════════════════════════════════════════════════════════════ +# Audio: <|audio|> marker preservation, and coexistence with adapters +# ════════════════════════════════════════════════════════════════════ + + +def _audio_messages(text="transcribe this", audio_type="audio"): + """A user turn whose content is a parts list carrying an audio clip.""" + return [ + { + "role": "user", + "content": [ + {"type": "text", "text": text}, + {"type": audio_type, audio_type: "clip-placeholder"}, + ], + } + ] + + +class TestAudioChatTemplate: + """configure_audio_chat_template emits the <|audio|> marker for audio parts.""" + + def test_audio_part_emits_marker(self): + tokenizer = _make_tokenizer() + configure_audio_chat_template(tokenizer) + result = _render( + tokenizer, messages=_audio_messages(), add_generation_prompt=True + ) + assert "<|audio|>" in result + assert "transcribe this" in result + + def test_marker_dropped_without_injection(self): + # Regression canary: the un-injected base template drops the audio part. + tokenizer = _make_tokenizer() + result = _render( + tokenizer, messages=_audio_messages(), add_generation_prompt=True + ) + assert "<|audio|>" not in result + assert "transcribe this" in result + + def test_input_audio_and_audio_url_types_emit_marker(self): + for audio_type in ("input_audio", "audio_url"): + tokenizer = _make_tokenizer() + configure_audio_chat_template(tokenizer) + result = _render( + tokenizer, + messages=_audio_messages(audio_type=audio_type), + add_generation_prompt=True, + ) + assert "<|audio|>" in result, f"marker missing for type={audio_type!r}" + + def test_custom_marker_string(self): + tokenizer = _make_tokenizer() + configure_audio_chat_template(tokenizer, marker="<|snd|>") + result = _render( + tokenizer, messages=_audio_messages(), add_generation_prompt=True + ) + assert "<|snd|>" in result + + def test_missing_anchor_raises(self): + tokenizer = SimpleNamespace(chat_template="{{ messages }}") + with pytest.raises(ValueError, match="content-part loop"): + configure_audio_chat_template(tokenizer) + + def test_none_template_is_noop(self): + tokenizer = SimpleNamespace(chat_template=None) + configure_audio_chat_template(tokenizer) + assert tokenizer.chat_template is None + + +class TestAudioAndAdapterInjectionsCompose: + """Adapter and audio injections applied in sequence leave both intact.""" + + def test_lora_prefix_and_audio_marker_coexist(self): + tokenizer = _make_tokenizer() + configure_chat_template(tokenizer, [("/path/a", "ctx_rel", "lora")]) + configure_audio_chat_template(tokenizer) + + result = _render( + tokenizer, + messages=_audio_messages(), + add_generation_prompt=True, + adapter_name="ctx_rel", + ) + assert result.startswith("<|ctx_rel|>"), result[:80] + assert "<|audio|>" in result + assert "transcribe this" in result + + def test_no_adapter_still_emits_audio_marker(self): + tokenizer = _make_tokenizer() + configure_chat_template(tokenizer, [("/path/a", "ctx_rel", "lora")]) + configure_audio_chat_template(tokenizer) + + result = _render( + tokenizer, messages=_audio_messages(), add_generation_prompt=True + ) + assert "<|audio|>" in result + assert "<|ctx_rel|>" not in result diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index cdc65a3..f25e2a7 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -158,3 +158,33 @@ def test_asr_kwargs_round_trip(self, tmp_path): assert loaded.asr_model_id == "openai/whisper-large-v3" assert loaded.asr_pipeline_kwargs == {"chunk_length_s": 15, "batch_size": 4} assert loaded.asr_generate_kwargs == {"language": "de", "task": "transcribe"} + + def test_adapters_and_audio_coexist_round_trip(self, tmp_path): + """Adapters and the audio cascade coexist and survive save→load.""" + cfg = GraniteSwitchConfig( + **_valid_kwargs(num_adapters=3), + asr_enabled=True, + asr_model_id="distil-whisper/distil-small.en", + asr_max_audio_clips=4, + ) + assert cfg.num_adapters == 3 + assert cfg.adapter_token_ids == [500, 501, 502] + assert cfg.asr_enabled is True + + cfg.save_pretrained(tmp_path) + loaded = GraniteSwitchConfig.from_pretrained(tmp_path) + assert loaded.num_adapters == 3 + assert loaded.adapter_token_ids == [500, 501, 502] + assert loaded.adapter_names == ["adapter_0", "adapter_1", "adapter_2"] + assert loaded.adapter_ranks == [8, 8, 8] + assert loaded.adapter_substitute_token_ids == [1, 1, 1] + assert loaded.asr_enabled is True + assert loaded.asr_model_id == "distil-whisper/distil-small.en" + assert loaded.asr_max_audio_clips == 4 + + def test_adapter_token_ids_do_not_collide_with_reserved_audio_row(self, tmp_path): + """Enabling audio does not perturb adapter token ids.""" + before = GraniteSwitchConfig(**_valid_kwargs(num_adapters=2)) + after = GraniteSwitchConfig(**_valid_kwargs(num_adapters=2), asr_enabled=True) + assert before.adapter_token_ids == after.adapter_token_ids + assert before.adapter_substitute_token_ids == after.adapter_substitute_token_ids diff --git a/tests/vllm/test_audio_processor.py b/tests/vllm/test_audio_processor.py index 5a69136..2d81f3d 100644 --- a/tests/vllm/test_audio_processor.py +++ b/tests/vllm/test_audio_processor.py @@ -315,3 +315,135 @@ def test_self_chunks_and_chunk_params_forwarded(self, monkeypatch): assert capture["self_chunks"] is False assert capture["chunk_length_s"] == 20.0 assert capture["chunk_overlap_s"] == 3.0 + + +def _make_processor_transcribing(info, monkeypatch, text, *, ids_for): + """Processor whose transcriber returns a fixed ``text``; ``ids_for`` maps it to ids.""" + info.get_tokenizer = lambda: SimpleNamespace( + encode=lambda t, add_special_tokens=False: ids_for(t) + ) + proc = object.__new__(GraniteSwitchASRMultiModalProcessor) + proc.info = info + + class FakeTranscriber: + def transcribe( + self, + audio, + sampling_rate=None, + generate_kwargs=None, + self_chunks=True, + chunk_length_s=30.0, + chunk_overlap_s=5.0, + ): + return text + + monkeypatch.setattr( + proc_mod, + "get_transcriber", + lambda model_id=None, device="cpu", pipeline_kwargs=None: FakeTranscriber(), + ) + return proc + + +class TestEmptyAndSilentClip: + """A silent clip transcribes to "" -> zero transcript tokens, marker elided.""" + + def _ids_for(self, text): + return [] if text == "" else [1, 2, 3] + + def test_empty_transcript_yields_zero_length_item(self, monkeypatch): + import torch + + info = _make_info(asr_enabled=True, asr_model_id="w") + proc = _make_processor_transcribing( + info, monkeypatch, "", ids_for=self._ids_for + ) + + bf = proc._call_hf_processor( + prompt="<|audio|>", + mm_data={"audios": [np.zeros(1600, dtype=np.float32)]}, + mm_kwargs={}, + tok_kwargs={}, + ) + assert bf["audio_num_tokens"].tolist() == [0] + assert len(bf["audio_token_ids"]) == 0 + assert bf["audio_token_ids"].dtype == torch.long + + def test_empty_clip_replacement_is_empty(self, monkeypatch): + info = _make_info(asr_enabled=True, asr_model_id="w") + proc = _make_processor_transcribing( + info, monkeypatch, "", ids_for=self._ids_for + ) + + class _Kwargs: + def __init__(self, data): + self._data = data + + def get_data(self): + return self._data + + import torch + + out = _Kwargs( + { + "audio_num_tokens": torch.tensor([0], dtype=torch.long), + "audio_token_ids": torch.zeros(0, dtype=torch.long), + } + ) + updates = proc._get_prompt_updates(None, {}, out) + assert len(updates) == 1 + assert updates[0].replacement(0) == [] + + def test_mixed_empty_and_nonempty_clips(self, monkeypatch): + info = _make_info(asr_enabled=True, asr_max_audio_clips=4, asr_model_id="w") + texts = iter(["", "words"]) + info.get_tokenizer = lambda: SimpleNamespace( + encode=lambda t, add_special_tokens=False: self._ids_for(t) + ) + proc = object.__new__(GraniteSwitchASRMultiModalProcessor) + proc.info = info + + class FakeTranscriber: + def transcribe(self, audio, **kw): + return next(texts) + + monkeypatch.setattr( + proc_mod, + "get_transcriber", + lambda model_id=None, device="cpu", pipeline_kwargs=None: FakeTranscriber(), + ) + + bf = proc._call_hf_processor( + prompt="<|audio|> <|audio|>", + mm_data={ + "audios": [ + np.zeros(1600, dtype=np.float32), + np.zeros(1600, dtype=np.float32), + ] + }, + mm_kwargs={}, + tok_kwargs={}, + ) + assert bf["audio_num_tokens"].tolist() == [0, 3] + assert len(bf["audio_token_ids"]) == 3 + + +class TestClipCeiling: + """get_supported_mm_limits publishes asr_max_audio_clips as the clip ceiling.""" + + def test_ceiling_equals_configured_max(self): + assert _make_info(asr_enabled=True).get_supported_mm_limits() == {"audio": 32} + assert _make_info( + asr_enabled=True, asr_max_audio_clips=1 + ).get_supported_mm_limits() == {"audio": 1} + assert _make_info( + asr_enabled=True, asr_max_audio_clips=8 + ).get_supported_mm_limits() == {"audio": 8} + + def test_per_item_bound_never_exceeds_context(self): + info = _make_info(asr_enabled=True, asr_max_audio_clips=32) + seq_len = 4096 + for count in (1, 2, 8, 32): + bound = info.get_mm_max_tokens_per_item(seq_len, {"audio": count})["audio"] + assert bound == seq_len // count + assert bound <= seq_len From 51b9be6f0dcb4ca0bf681eede577456cc2854046 Mon Sep 17 00:00:00 2001 From: BAR HAIM Date: Mon, 27 Jul 2026 13:58:47 +0000 Subject: [PATCH 2/5] tests: add e2e vLLM audio serving smoke (text + adapter + audio x1/x2/x3) CI-runnable serving smoke closing the #47 e2e box. Boots a real composed, audio-enabled GraniteSwitch checkpoint under vLLM and drives text-only, adapter-control-token, and audio x1/x2/x3 requests through one live engine. Markers slow+requires_model+gpu (opt-in via -m); model built through the compose CLI with --enable-audio; synthetic audio keeps it asset-free. Verified green on an A100 with ibm-granite/granite-4.1-3b + granitelib-core-r1.0: 5 passed. Signed-off-by: BAR HAIM --- tests/integration/test_audio_serving_smoke.py | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 tests/integration/test_audio_serving_smoke.py diff --git a/tests/integration/test_audio_serving_smoke.py b/tests/integration/test_audio_serving_smoke.py new file mode 100644 index 0000000..b1eb3a6 --- /dev/null +++ b/tests/integration/test_audio_serving_smoke.py @@ -0,0 +1,228 @@ +# SPDX-License-Identifier: Apache-2.0 +"""End-to-end vLLM serving smoke for the audio cascade (issue #47). + +Boots a real composed, audio-enabled GraniteSwitch checkpoint under vLLM and +drives the three request shapes the #47 checklist calls out as the serving +smoke — **text + adapter + audio x1/x2/x3** — through one live engine. This is +the path the low-level tests deliberately bypass (CUDA graphs, torch.compile, +the ASR processor running inside vLLM's EngineCore subprocess, the multi-clip +splice, and generation), so it is the only test that proves those pieces +compose at serve time. + +Scope is a *smoke*: each request must complete and return well-formed output, +and multi-clip requests must be accepted up to the checkpoint's declared clip +ceiling. It intentionally does NOT assert transcript content — transcription +quality (WER) and adapter-routing correctness are separate #47 boxes covered by +the eval harness and `test_switch_e2e_compose.py` respectively. Synthetic audio +keeps the test asset-free; a silent/tonal clip exercises the serving plumbing +without shipping a speech fixture. + +Model construction goes through the compose CLI (CLAUDE.md gotcha #5): no test +hand-assembles a config. Audio is enabled with `--enable-audio`, which defaults +the ASR front-end to the small built-in model (distil-whisper/distil-small.en) +to keep CI cost down. + +Markers: @pytest.mark.slow + @pytest.mark.requires_model + @pytest.mark.gpu. +CI must opt in explicitly: `pytest -m "slow and requires_model and gpu"`. +""" + +import importlib.util +import json +import os + +import pytest + +pytestmark = [pytest.mark.slow, pytest.mark.requires_model, pytest.mark.gpu] + +if importlib.util.find_spec("vllm") is None: + pytest.skip("requires vLLM installed", allow_module_level=True) + + +# ---------------------------------------------------------------------------- +# Base-model / adapter-library pairs — kept in lockstep with +# tests/integration/test_switch_e2e_compose.py so the two E2E files exercise +# the same model matrix. A fast CI profile can pin one pair via -k or the +# experimental env var below. +# ---------------------------------------------------------------------------- + +_DEFAULT_BASE_MODEL_PAIRS = [ + ("ibm-granite/granite-4.0-micro", "ibm-granite/granitelib-core-r1.0"), + ("ibm-granite/granite-4.1-3b", "ibm-granite/granitelib-core-r1.0"), +] + + +def _load_experimental_pairs(): + """Local/experimental pairings from GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS. + + JSON array of {"base": str, "adapter": str}; base/adapter may be HF ids or + local paths. The mechanism is committed; the values are not. Mirrors + test_switch_e2e_compose.py so both E2E files share one extension knob. + """ + raw = os.environ.get("GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS", "") + if not raw: + return [] + try: + entries = json.loads(raw) + except json.JSONDecodeError as e: + raise ValueError( + f"GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS is not valid JSON: {e}\n" + f'Expected format: \'[{{"base":"/path","adapter":"/path"}}, ...]\'' + ) + return [(p["base"], p["adapter"]) for p in entries] + + +BASE_MODEL_PAIRS = _DEFAULT_BASE_MODEL_PAIRS + _load_experimental_pairs() + +COMPOSE_TIMEOUT_S = 1800 # 30 min — matches test_switch_e2e_compose.py +_TARGET_SR = 16_000 + + +@pytest.fixture( + scope="module", + params=BASE_MODEL_PAIRS, + ids=lambda p: p[0].rsplit("/", 1)[-1], +) +def audio_checkpoint(request, tmp_path_factory): + """Compose one audio-enabled checkpoint per (base, adapter) pair. + + Delegates to the compose CLI (same as test_switch_e2e_compose.py) with + `--enable-audio`, then returns the save dir. Module scope amortizes the + download-dominated first run across every smoke case for the pair. + """ + import subprocess + import sys + + base_model, adapter_library = request.param + save_dir = tmp_path_factory.mktemp(base_model.rsplit("/", 1)[-1]) / "model" + + cmd = [ + sys.executable, + "-m", + "granite_switch.composer.compose_granite_switch", + "--base-model", + base_model, + "--adapters", + adapter_library, + "--enable-audio", + "--output", + str(save_dir), + ] + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=COMPOSE_TIMEOUT_S + ) + if result.returncode != 0: + raise RuntimeError( + f"compose failed for base={base_model} adapter={adapter_library}\n" + f"--- STDOUT ---\n{result.stdout}\n--- STDERR ---\n{result.stderr}" + ) + return {"base_model": base_model, "save_dir": save_dir} + + +@pytest.fixture(scope="module") +def served(audio_checkpoint): + """Boot vLLM once for the checkpoint and share it across smoke cases. + + Tokenizer init stays ON (unlike the argmax-equivalence test): the ASR + processor needs a tokenizer to encode both the prompt and the transcript, + and the adapter case needs it to tokenize text around the control token. + """ + import gc + + import torch + + os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") + from vllm import LLM + + llm = LLM( + model=str(audio_checkpoint["save_dir"]), + dtype="bfloat16", + gpu_memory_utilization=0.7, + enforce_eager=True, # smoke: skip CUDA-graph capture for a faster boot + ) + try: + yield {"llm": llm, "config": llm.llm_engine.model_config.hf_config} + finally: + del llm + gc.collect() + torch.cuda.empty_cache() + + +def _tone(seconds: float = 1.0, freq: float = 440.0): + """Deterministic mono 16 kHz waveform (no speech fixture needed).""" + import numpy as np + + t = np.arange(int(seconds * _TARGET_SR), dtype=np.float32) / _TARGET_SR + return (0.1 * np.sin(2 * np.pi * freq * t)).astype(np.float32) + + +def _one_completion(outputs): + """Assert a single RequestOutput carrying at least one generated token.""" + assert len(outputs) == 1 + completion = outputs[0].outputs[0] + assert len(completion.token_ids) >= 1 + return completion + + +def test_text_only_serving(served): + """Baseline: a plain text request serves normally (backward-compat).""" + from vllm import SamplingParams + + outputs = served["llm"].generate( + "The capital of France is", + SamplingParams(max_tokens=8, temperature=0.0), + ) + _one_completion(outputs) + + +def test_adapter_control_token_serving(served): + """An adapter control token routes through the switch under serving. + + Correctness of the routing is `test_switch_e2e_compose.py`'s job; here we + only prove the switch path runs end-to-end in the live engine without + crashing and still generates. + """ + from vllm import SamplingParams + from vllm.inputs import TokensPrompt + + config = served["config"] + if not getattr(config, "adapter_token_ids", None): + pytest.skip("composed checkpoint has no adapters") + + tokenizer = served["llm"].get_tokenizer() + text_ids = tokenizer.encode("Summarize the document.", add_special_tokens=False) + # LORA control tokens sit at the sequence start (CLAUDE.md gotcha #3). + prompt = TokensPrompt(prompt_token_ids=[config.adapter_token_ids[0], *text_ids]) + + outputs = served["llm"].generate( + prompt, SamplingParams(max_tokens=8, temperature=0.0) + ) + _one_completion(outputs) + + +@pytest.mark.parametrize("num_clips", [1, 2, 3]) +def test_audio_clip_serving(served, num_clips): + """Audio x1/x2/x3: N markers + N clips transcribe, splice, and generate. + + Exercises the multi-clip long-audio serving path through the ASR processor + running inside vLLM's engine subprocess. Content is not asserted (WER is a + separate #47 box); the bar is a completed request with well-formed output. + """ + from vllm import SamplingParams + + ceiling = int(getattr(served["config"], "asr_max_audio_clips", 32) or 32) + if num_clips > ceiling: + pytest.skip(f"checkpoint clip ceiling {ceiling} < {num_clips}") + + marker = "<|audio|>" + prompt = { + "prompt": marker * num_clips + " What was said?", + "multi_modal_data": { + "audio": [ + (_tone(freq=220.0 * (i + 1)), _TARGET_SR) for i in range(num_clips) + ] + }, + } + outputs = served["llm"].generate( + prompt, SamplingParams(max_tokens=8, temperature=0.0) + ) + _one_completion(outputs) From 9181d4f515d2be9ce457015167fa27c82b03d0e2 Mon Sep 17 00:00:00 2001 From: BAR HAIM Date: Mon, 27 Jul 2026 16:55:55 +0000 Subject: [PATCH 3/5] tests: e2e answerability adapter over an audio-delivered document (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the first correctness test for the "adapters + audio together" #47 box: the RAG answerability adapter judging a query against a document supplied as speech rather than text. The document is delivered as audio by placing a single <|audio|> marker as the document text (documents=[{"text": "<|audio|>"}]); the Granite template renders each document via `doc | tojson`, so the marker lands in the block exactly where vLLM's ASR processor splices the transcript — the document the adapter reasons over is the transcribed speech. The <|answerability|> control token is inserted by the same template (aLoRA fallback path), so the switch fires the adapter at the generation prompt. Ground truth comes from one real clip (hf-internal-testing/ librispeech_asr_dummy row 0), driving both classes without shipping a fixture: an answerable question whose answer is spoken, and an unanswerable one about content the clip never mentions. Both are distinctive enough to survive ASR word-errors, so the assertion tests the answerability decision, not transcription accuracy (WER is a separate box). FLAC is decoded with soundfile from the raw bytes to avoid requiring torchcodec (datasets>=4). Where test_audio_serving_smoke.py only proves an adapter control token doesn't crash the audio path, this proves the switch produces the correct verdict end-to-end through a live vLLM engine. Verified on an A100 with granite-4.1-3b + granitelib-rag-r1.0: 2 passed. Markers slow+requires_model+gpu; opt-in via pytest -m "slow and requires_model and gpu" Signed-off-by: BAR HAIM --- .../test_answerability_over_audio.py | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 tests/integration/test_answerability_over_audio.py diff --git a/tests/integration/test_answerability_over_audio.py b/tests/integration/test_answerability_over_audio.py new file mode 100644 index 0000000..b4e85d9 --- /dev/null +++ b/tests/integration/test_answerability_over_audio.py @@ -0,0 +1,225 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Adapters + audio: the RAG answerability adapter judging a query against a +document delivered as audio (issue #47). + +Covers the "adapters + audio" box. Where test_audio_serving_smoke.py only proves +an adapter control token doesn't crash the audio path, this checks the switch +reaches the *correct* verdict when the RAG document arrives as speech. + +Document-as-audio: ``documents=[{"text": "<|audio|>"}]`` — the Granite template +renders each document with ``doc | tojson``, so the marker lands in the +```` block where the ASR processor splices the transcript. The +``<|answerability|>`` control token is inserted by the same template (aLoRA +fallback path). Ground truth is one real clip (librispeech_asr_dummy row 0) +driving both classes; the questions are distinctive enough to survive ASR +word-errors, so this tests the decision, not transcription accuracy (WER). + +Markers: slow + requires_model + gpu (opt-in via -m). +""" + +import importlib.util +import json +import os + +import pytest + +pytestmark = [pytest.mark.slow, pytest.mark.requires_model, pytest.mark.gpu] + +if importlib.util.find_spec("vllm") is None: + pytest.skip("requires vLLM installed", allow_module_level=True) + + +_DEFAULT_BASE_MODEL_PAIRS = [ + ("ibm-granite/granite-4.1-3b", "ibm-granite/granitelib-rag-r1.0"), +] + + +def _load_experimental_pairs(): + """Extra (base, adapter) pairs from GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS. + + JSON array of {"base": str, "adapter": str}; mirrors the other E2E files. + """ + raw = os.environ.get("GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS", "") + if not raw: + return [] + try: + entries = json.loads(raw) + except json.JSONDecodeError as e: + raise ValueError( + f"GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS is not valid JSON: {e}\n" + f'Expected format: \'[{{"base":"/path","adapter":"/path"}}, ...]\'' + ) + return [(p["base"], p["adapter"]) for p in entries] + + +BASE_MODEL_PAIRS = _DEFAULT_BASE_MODEL_PAIRS + _load_experimental_pairs() + +COMPOSE_TIMEOUT_S = 1800 # matches the other E2E compose fixtures +_AUDIO_MARKER = "<|audio|>" + +# librispeech_asr_dummy row 0; the fixture soft-checks the live reference so a +# dataset change skips rather than silently mis-tests. +_EXPECTED_REF_SUBSTRING = "apostle of the middle classes" +_ANSWERABILITY_CASES = [ + ("Who is the apostle of the middle classes?", "answerable"), + ("What is the boiling point of water?", "unanswerable"), +] + + +@pytest.fixture( + scope="module", + params=BASE_MODEL_PAIRS, + ids=lambda p: p[0].rsplit("/", 1)[-1], +) +def audio_rag_checkpoint(request, tmp_path_factory): + """Compose one audio-enabled RAG checkpoint per (base, adapter) pair.""" + import subprocess + import sys + + base_model, adapter_library = request.param + save_dir = tmp_path_factory.mktemp(base_model.rsplit("/", 1)[-1]) / "model" + + cmd = [ + sys.executable, + "-m", + "granite_switch.composer.compose_granite_switch", + "--base-model", + base_model, + "--adapters", + adapter_library, + "--enable-audio", + "--output", + str(save_dir), + ] + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=COMPOSE_TIMEOUT_S + ) + if result.returncode != 0: + raise RuntimeError( + f"compose failed for base={base_model} adapter={adapter_library}\n" + f"--- STDOUT ---\n{result.stdout}\n--- STDERR ---\n{result.stderr}" + ) + return {"base_model": base_model, "save_dir": save_dir} + + +@pytest.fixture(scope="module") +def served(audio_rag_checkpoint): + """Boot vLLM once for the checkpoint and share it across the cases.""" + import gc + + import torch + + os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") + from vllm import LLM + + llm = LLM( + model=str(audio_rag_checkpoint["save_dir"]), + dtype="bfloat16", + gpu_memory_utilization=0.7, + enforce_eager=True, + ) + try: + yield { + "llm": llm, + "config": llm.llm_engine.model_config.hf_config, + "tokenizer": llm.get_tokenizer(), + } + finally: + del llm + gc.collect() + torch.cuda.empty_cache() + + +def _answerability_adapter_name(config): + # Discovery derives the name from the library layout — match rather than + # hard-code, and skip loudly if this checkpoint has no answerability adapter. + names = list(getattr(config, "adapter_names", None) or []) + for name in names: + if "answerab" in name.lower(): + return name + pytest.skip(f"no answerability adapter in composed checkpoint (adapters={names})") + + +@pytest.fixture(scope="module") +def librispeech_clip(): + """One real 16 kHz speech clip + reference transcript (row 0).""" + import io + + import soundfile as sf + from datasets import Audio, load_dataset + + ds = load_dataset( + "hf-internal-testing/librispeech_asr_dummy", "clean", split="validation" + ) + # Decode FLAC bytes with soundfile: datasets>=4 defers audio decoding to + # torchcodec, an optional dep we don't want to require of the test env. + ds = ds.cast_column("audio", Audio(decode=False)) + row = ds[0] + reference = row["text"] + if _EXPECTED_REF_SUBSTRING not in reference.lower(): + pytest.skip( + "librispeech_asr_dummy row 0 changed; expected reference containing " + f"{_EXPECTED_REF_SUBSTRING!r}, got {reference!r}" + ) + audio = row["audio"] + raw = audio["bytes"] if audio.get("bytes") else open(audio["path"], "rb").read() + waveform, sr = sf.read(io.BytesIO(raw), dtype="float32") + if waveform.ndim > 1: + waveform = waveform.mean(axis=1) + return {"waveform": waveform, "sr": sr, "reference": reference} + + +def _build_answerability_prompt(tokenizer, adapter_name, question): + # documents=[{"text": "<|audio|>"}] places one audio marker in the template's + # block; adapter_name arms the answerability control-token insert. + return tokenizer.apply_chat_template( + [{"role": "user", "content": question}], + documents=[{"text": _AUDIO_MARKER}], + add_generation_prompt=True, + adapter_name=adapter_name, + tokenize=False, + ) + + +def _parse_label(text): + # Adapter emits the bare enum ("answerable"/"unanswerable"), maybe quoted. + # Match the longer label first so "answerable" doesn't shadow "unanswerable". + norm = text.strip().strip('"').strip().lower() + if "unanswerable" in norm: + return "unanswerable" + if "answerable" in norm: + return "answerable" + return norm # unrecognized — surfaced by the assertion + + +@pytest.mark.parametrize( + "question,expected", + _ANSWERABILITY_CASES, + ids=[c[1] for c in _ANSWERABILITY_CASES], +) +def test_answerability_over_audio_document( + served, librispeech_clip, question, expected +): + """Answerability adapter reaches the correct verdict on an audio document.""" + from vllm import SamplingParams + + adapter_name = _answerability_adapter_name(served["config"]) + prompt = _build_answerability_prompt(served["tokenizer"], adapter_name, question) + + outputs = served["llm"].generate( + { + "prompt": prompt, + "multi_modal_data": { + "audio": [(librispeech_clip["waveform"], librispeech_clip["sr"])] + }, + }, + SamplingParams(max_tokens=8, temperature=0.0), + ) + + assert len(outputs) == 1 + raw = outputs[0].outputs[0].text + label = _parse_label(raw) + assert label == expected, ( + f"answerability mismatch for {question!r}: got {label!r} " + f"(raw={raw!r}), expected {expected!r}" + ) From 26e8be597126f17bd101b77b81cfb737ca2dba32 Mon Sep 17 00:00:00 2001 From: BAR HAIM Date: Mon, 27 Jul 2026 19:55:55 +0000 Subject: [PATCH 4/5] tests: use committed local speech clip for answerability-over-audio Replace the runtime hf-internal-testing/librispeech_asr_dummy download with a committed 3s 16 kHz WAV (SpeechT5, MIT-licensed) so the test runs offline and carries clean provenance for merge-to-main. The spoken sentence states a location that distil-whisper transcribes cleanly, so the questions key on the location rather than the proper noun. Verdicts verified: 2 passed on A100 (granite-4.1-3b + granitelib-rag-r1.0). Signed-off-by: BAR HAIM --- .../fixtures/eiffel_tower_paris.wav | Bin 0 -> 98348 bytes .../test_answerability_over_audio.py | 49 ++++++------------ 2 files changed, 16 insertions(+), 33 deletions(-) create mode 100644 tests/integration/fixtures/eiffel_tower_paris.wav diff --git a/tests/integration/fixtures/eiffel_tower_paris.wav b/tests/integration/fixtures/eiffel_tower_paris.wav new file mode 100644 index 0000000000000000000000000000000000000000..d88341dd181684a839cbfbe397d32a1d04cebd2c GIT binary patch literal 98348 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 literal 0 HcmV?d00001 diff --git a/tests/integration/test_answerability_over_audio.py b/tests/integration/test_answerability_over_audio.py index b4e85d9..e59f11b 100644 --- a/tests/integration/test_answerability_over_audio.py +++ b/tests/integration/test_answerability_over_audio.py @@ -10,9 +10,10 @@ renders each document with ``doc | tojson``, so the marker lands in the ```` block where the ASR processor splices the transcript. The ``<|answerability|>`` control token is inserted by the same template (aLoRA -fallback path). Ground truth is one real clip (librispeech_asr_dummy row 0) -driving both classes; the questions are distinctive enough to survive ASR -word-errors, so this tests the decision, not transcription accuracy (WER). +fallback path). Ground truth is one committed speech clip (fixtures/, generated +with SpeechT5 — MIT) driving both classes; the questions key on content that +transcribes cleanly, so this tests the decision, not transcription accuracy +(WER). Markers: slow + requires_model + gpu (opt-in via -m). """ @@ -20,6 +21,7 @@ import importlib.util import json import os +from pathlib import Path import pytest @@ -57,11 +59,12 @@ def _load_experimental_pairs(): COMPOSE_TIMEOUT_S = 1800 # matches the other E2E compose fixtures _AUDIO_MARKER = "<|audio|>" -# librispeech_asr_dummy row 0; the fixture soft-checks the live reference so a -# dataset change skips rather than silently mis-tests. -_EXPECTED_REF_SUBSTRING = "apostle of the middle classes" +# Committed speech clip; spoken sentence is "The Eiffel Tower is located in the +# city of Paris in France." The location words transcribe cleanly (the proper +# noun does not, hence the questions key on the location, not the name). +_AUDIO_FIXTURE = Path(__file__).parent / "fixtures" / "eiffel_tower_paris.wav" _ANSWERABILITY_CASES = [ - ("Who is the apostle of the middle classes?", "answerable"), + ("In which city is the tower located?", "answerable"), ("What is the boiling point of water?", "unanswerable"), ] @@ -141,32 +144,14 @@ def _answerability_adapter_name(config): @pytest.fixture(scope="module") -def librispeech_clip(): - """One real 16 kHz speech clip + reference transcript (row 0).""" - import io - +def speech_clip(): + """The committed 16 kHz mono speech clip as (waveform, sample_rate).""" import soundfile as sf - from datasets import Audio, load_dataset - ds = load_dataset( - "hf-internal-testing/librispeech_asr_dummy", "clean", split="validation" - ) - # Decode FLAC bytes with soundfile: datasets>=4 defers audio decoding to - # torchcodec, an optional dep we don't want to require of the test env. - ds = ds.cast_column("audio", Audio(decode=False)) - row = ds[0] - reference = row["text"] - if _EXPECTED_REF_SUBSTRING not in reference.lower(): - pytest.skip( - "librispeech_asr_dummy row 0 changed; expected reference containing " - f"{_EXPECTED_REF_SUBSTRING!r}, got {reference!r}" - ) - audio = row["audio"] - raw = audio["bytes"] if audio.get("bytes") else open(audio["path"], "rb").read() - waveform, sr = sf.read(io.BytesIO(raw), dtype="float32") + waveform, sr = sf.read(_AUDIO_FIXTURE, dtype="float32") if waveform.ndim > 1: waveform = waveform.mean(axis=1) - return {"waveform": waveform, "sr": sr, "reference": reference} + return {"waveform": waveform, "sr": sr} def _build_answerability_prompt(tokenizer, adapter_name, question): @@ -197,9 +182,7 @@ def _parse_label(text): _ANSWERABILITY_CASES, ids=[c[1] for c in _ANSWERABILITY_CASES], ) -def test_answerability_over_audio_document( - served, librispeech_clip, question, expected -): +def test_answerability_over_audio_document(served, speech_clip, question, expected): """Answerability adapter reaches the correct verdict on an audio document.""" from vllm import SamplingParams @@ -210,7 +193,7 @@ def test_answerability_over_audio_document( { "prompt": prompt, "multi_modal_data": { - "audio": [(librispeech_clip["waveform"], librispeech_clip["sr"])] + "audio": [(speech_clip["waveform"], speech_clip["sr"])] }, }, SamplingParams(max_tokens=8, temperature=0.0), From f66adfcfda28c2deedf72395ef5c775c8b6ac089 Mon Sep 17 00:00:00 2001 From: BAR HAIM Date: Mon, 27 Jul 2026 20:38:04 +0000 Subject: [PATCH 5/5] tests: sweep all default adapters route correctly with audio enabled Adds the 'all default adapters route correctly with audio enabled' coverage for issue #47. Composes the full default set (RAG + Core + Guardian) with --enable-audio and asserts the switch maps each adapter_token_ids[i] to index i+1 (pre-control positions stay on base 0), observed via the HF backend's _last_adapter_indices. For granite-4.1-3b, 12 adapters resolve (context_relevance has no 4.1-3b flavor); verified 12/12 route correctly on A100. Loads with ignore_mismatched_sizes=True: the <|audio|> token bumps vocab_size one past the switch's control_to_substitute_lut buffer, which SingleSwitch rebuilds from config, so the freshly-built buffer is kept. Signed-off-by: BAR HAIM --- .../test_adapter_routing_audio_enabled.py | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 tests/integration/test_adapter_routing_audio_enabled.py diff --git a/tests/integration/test_adapter_routing_audio_enabled.py b/tests/integration/test_adapter_routing_audio_enabled.py new file mode 100644 index 0000000..131a01f --- /dev/null +++ b/tests/integration/test_adapter_routing_audio_enabled.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Adapters + audio (issue #47): every default adapter routes to its own index +on an audio-enabled checkpoint. + +Composes the default adapter set (RAG + Core + Guardian) with --enable-audio and +sweeps every adapter control token that resolved, asserting the switch maps +``adapter_token_ids[i]`` to index ``i + 1`` and leaves pre-control positions on +the base (``0``) — i.e. enabling audio does not perturb the token->index map. +For granite-4.1-3b that is 12 adapters: context_relevance ships no 4.1-3b flavor, +so 12 of the 13 defined adapters resolve. Complements the single-adapter sweep in +test_switch_e2e_compose and the serve-time answerability check in +test_answerability_over_audio. + +Markers: slow + requires_model + gpu (opt-in via -m). +""" + +import importlib.util +import json +import os + +import pytest + +pytestmark = [pytest.mark.slow, pytest.mark.requires_model, pytest.mark.gpu] + +if importlib.util.find_spec("granite_switch.hf") is None: + pytest.skip("requires the HF backend ([hf] extra)", allow_module_level=True) + + +# The default adapter set is the union of the three granitelib libraries (the +# "12 adapters"): RAG + Core + Guardian. +_DEFAULT_ADAPTER_LIBRARIES = [ + "ibm-granite/granitelib-rag-r1.0", + "ibm-granite/granitelib-core-r1.0", + "ibm-granite/granitelib-guardian-r1.0", +] +_DEFAULT_BASE_MODEL_PAIRS = [ + ("ibm-granite/granite-4.1-3b", _DEFAULT_ADAPTER_LIBRARIES), +] + + +def _load_experimental_pairs(): + raw = os.environ.get("GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS", "") + if not raw: + return [] + try: + entries = json.loads(raw) + except json.JSONDecodeError as e: + raise ValueError( + f"GRANITE_SWITCH_EXPERIMENTAL_MODEL_PAIRS is not valid JSON: {e}\n" + f'Expected format: \'[{{"base":"/path","adapter":"/path"}}, ...]\'' + ) + # adapter may be a single library or a list of libraries. + pairs = [] + for p in entries: + adapters = p["adapter"] + pairs.append( + (p["base"], adapters if isinstance(adapters, list) else [adapters]) + ) + return pairs + + +BASE_MODEL_PAIRS = _DEFAULT_BASE_MODEL_PAIRS + _load_experimental_pairs() + +COMPOSE_TIMEOUT_S = 1800 +_SEQ_LEN = 8 +_CTRL_POS = 1 +_FILLER_TOKENS = [791, 5679, 2766, 279, 893, 389, 813, 1450] + + +@pytest.fixture( + scope="module", + params=BASE_MODEL_PAIRS, + ids=lambda p: p[0].rsplit("/", 1)[-1], +) +def audio_switch_model(request, tmp_path_factory): + import subprocess + import sys + + import torch + + from granite_switch.hf import GraniteSwitchForCausalLM + + base_model, adapter_libraries = request.param + save_dir = tmp_path_factory.mktemp(base_model.rsplit("/", 1)[-1]) / "model" + + cmd = [ + sys.executable, + "-m", + "granite_switch.composer.compose_granite_switch", + "--base-model", + base_model, + "--adapters", + *adapter_libraries, + "--enable-audio", + "--output", + str(save_dir), + ] + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=COMPOSE_TIMEOUT_S + ) + if result.returncode != 0: + raise RuntimeError( + f"compose failed for base={base_model} adapters={adapter_libraries}\n" + f"--- STDOUT ---\n{result.stdout}\n--- STDERR ---\n{result.stderr}" + ) + + # ignore_mismatched_sizes: the <|audio|> token bumps vocab_size one past the + # switch's control_to_substitute_lut buffer, but that buffer is rebuilt from + # config in SingleSwitch.__init__, so the freshly-built one is kept intact. + model = ( + GraniteSwitchForCausalLM.from_pretrained( + str(save_dir), dtype=torch.bfloat16, ignore_mismatched_sizes=True + ) + .eval() + .cuda() + ) + return {"base_model": base_model, "model": model, "config": model.config} + + +def _adapter_indices_after_forward(model, control_token_id): + import torch + + seq = [_FILLER_TOKENS[i % len(_FILLER_TOKENS)] for i in range(_SEQ_LEN)] + seq[_CTRL_POS] = control_token_id + with torch.no_grad(): + model(input_ids=torch.tensor([seq], device="cuda")) + return model.model._last_adapter_indices[0] + + +def test_all_adapters_route_with_audio_enabled(audio_switch_model): + """Each adapter control token routes to its own index on an audio checkpoint.""" + config = audio_switch_model["config"] + base_model = audio_switch_model["base_model"] + + assert getattr(config, "asr_enabled", False) is True, ( + f"checkpoint is not audio-enabled (base_model={base_model})" + ) + + token_ids = list(getattr(config, "adapter_token_ids", None) or []) + names = list(getattr(config, "adapter_names", None) or []) + assert token_ids, f"composed checkpoint has no adapters (base_model={base_model})" + print(f"\n sweeping {len(token_ids)} adapters (base_model={base_model})") + + failures = [] + for i, token_id in enumerate(token_ids): + expected = i + 1 # adapter_token_ids[i] activates adapter i+1; 0 = base + name = names[i] if i < len(names) else f"adapter_{expected}" + ai = _adapter_indices_after_forward(audio_switch_model["model"], token_id) + + ok = bool((ai[:_CTRL_POS] == 0).all()) and bool( + (ai[_CTRL_POS:] == expected).all() + ) + print( + f" [{'ok' if ok else 'FAIL'}] idx {expected:>2} {name!r}: {ai.tolist()}" + ) + if not ok: + failures.append((expected, name, token_id, ai.tolist())) + + assert not failures, ( + f"{len(failures)} adapter(s) mis-routed with audio enabled " + f"(base_model={base_model}); expected pre-control=0, post-control=index:\n" + + "\n".join( + f" idx {idx} {name!r} (token {tok}): {indices}" + for idx, name, tok, indices in failures + ) + )