Refactor/multiswitch only and conversation ids - #131
Merged
yairallouche merged 17 commits intoSep 10, 2026
Conversation
The doc was a standalone styled HTML page, which meant it rendered as raw markup on GitHub and sat outside the pre-commit link validator: that hook only reads tracked .md, .ipynb and .py files, and .html is absent from its EXT_OK set, so every link pointing AT the page was unchecked too. As Markdown it renders in-place and its inbound links are now validated. Structure is preserved: all 10 sections, 18 tables, and every ASCII diagram and verbatim trace. Typographic punctuation in prose is ASCII (the box-drawing characters in the diagrams are kept, being load-bearing); the table of contents is now anchor-linked. Pointers repointed at the new extension: CLAUDE.md gotcha 10 src/granite_switch/vllm/switch/multi.py:361 the 188-bound comment tutorials/notebooks/hello_multiswitch.ipynb tutorials/notebooks/multi_turn_multiswitch.ipynb tutorials/notebooks/multiswitch_serving.ipynb Content corrections made while converting: - switch_type's default is "multi" as of the next commit on this branch, so the comparison table and the config-surface table say so. Added a short subsection explaining why the two create_switch() call sites keep a "single" getattr fallback: it only fires for a config.json predating c5e78c6, whose num_hidden_layers was inflated by 1 rather than 2. - Stale line references corrected against this branch: config.py rows in the config-surface table, modeling_granite_switch.py:214-216 -> :343-344, conversation.py:99 -> :110. Remaining line references are carried over from the HTML and were not re-audited; the doc header now says so rather than implying otherwise. Signed-off-by: noaa <noaa.kless@ibm.com>
…t_payload Both policies reuse previously-sent ids as a stable prefix, so both must travel as ids. requires_token_ids is now constant True and the chat_payload method is removed (it returned a messages body the chat endpoint re-renders server-side, unable to carry ids). The /v1/chat/completions endpoint itself is unaffected; callers that want it just do not route through Conversation. conversation.py also carries the branch's SingleSwitch-removal cleanup of the old switch_type='multi' guard. Signed-off-by: noaa <noaa.kless@ibm.com>
Adds _appendable (control-token position decides whether the new turn can be a delta) and _control_char_index. A turn whose control token lands inside the already-sent prefix -- a LoRA token at index 0, or an aLoRA invocation in an earlier message -- can no longer be a delta; instead of raising, build_prompt re-prefills (full render, history's control tokens dropped). _reprefill is generalized with a reason so budget and placement fallbacks log accurately. A genuinely non-append-only template (Granite 4.2 thinking truncation) still raises: the discriminator is whether a no-adapter render extends the prefix. _reject_lora_placement is removed; _explain_no_prefix reduced to the template case. No adapter-technology detection anywhere. Signed-off-by: noaa <noaa.kless@ibm.com>
RE_PREFILL now carries _re_base_ids/_re_base_text: the base-rendered history prefix already sent (control tokens dropped), reused verbatim next turn so the prefix cache hits. build_prompt renders history with adapter=None (answers are already control-token-free, so this is base by construction) and appends the current turn's delta; a turn's base form is only sent the turn after it, so reuse lags one turn. Turn 1, a LoRA turn, or a non-append-only template full-renders. Output is byte-identical to a from-scratch RE_PREFILL render -- reuse is a cache optimization, never a change to what the model sees. Signed-off-by: noaa <noaa.kless@ibm.com>
Confirms the bf16 counting-head budget guard is PRESERVE-only: RE_PREFILL demotes history to base, so 50 turns each carry at most the current turn's one control token and never approach MAX_RETAINED_CONTROL_TOKENS. (The PRESERVE unappendable->reprefill wiring landed with Task 2.) Signed-off-by: noaa <noaa.kless@ibm.com>
RE_PREFILL now reuses base-demoted ids (lag one turn); an unappendable turn (LoRA at position 0, or an aLoRA trigger in an earlier turn) re-prefills instead of raising; chat_payload is removed and requires_token_ids is always true. Rewrites the two-policies table, the delta logic, the API surface, the refusals->fallback section, and recomputes the PRESERVE matrix with exact CPU-verified id counts. The one remaining raise is a non-append-only template. Test-inventory counts refreshed. Signed-off-by: noaa <noaa.kless@ibm.com>
Section 8 rewritten: PRESERVE no longer refuses a LoRA or stale-invocation turn, it re-prefills (demos now assert reprefills increments rather than a raise); chat_payload and the switch_type='single' demo are gone; the one remaining constructor raise is config-omitted, and the one build_prompt raise is a non-append-only template. Removes the dropped --switch-type CLI flag and the switch_type persistence assertion from section 9 (SingleSwitch is gone), and updates the intro, RE_PREFILL id-reuse framing, and the 188-ceiling note (re-prefill, not raise). Reprefill claims are backed by the unit tests. Signed-off-by: noaa <noaa.kless@ibm.com>
SingleSwitch is deleted from src, but the doc still described it as a live engine. create_switch no longer dispatches (it returns MultiSwitch directly); switch_type is no longer a config parameter but a rejection marker in from_dict. Rewrites the create_switch snippet, the switch_type-default table (now the rejection behavior), the comparison table (SingleSwitch marked removed, deleted-file reference dropped), the config-surface table (switch_type row removed), the layer-offset comment, and the base-reset row (dropped the removed validate_base_reset_switch_type). Also fixes the section-6 delta pseudocode: LoRA/earlier-trigger re-prefills, only a non-append-only template raises. Test reference updated to test_single_switch_rejected.py. Signed-off-by: noaa <noaa.kless@ibm.com>
SingleSwitch's single base->adapter transition (±gain cumsum over one attention head) is superseded by MultiSwitch's Kerdock/DG coded memory, which routes arbitrarily many transitions per request, latest-wins, and supports return-to-base. SingleSwitch averaged competing control tokens and mis-routed, and had no mechanism to re-select base mid-sequence. Delete both backends' single.py and all SingleSwitch-only tests and helpers (test_single_switch*, single_switch_cases, sequences, test_sharpness_equivalence; test_token_exchange's vLLM copy). create_switch builds MultiSwitch directly (no engine dispatch); from_dict rejects SingleSwitch/legacy checkpoints (pinned by test_single_switch_rejected.py); MultiSwitch owns num_cache_layers == 2 (counting + memory), where SingleSwitch owned 1. Signed-off-by: noaa <noaa.kless@ibm.com>
Four tests were added or shaped on main after this branch forked, while SingleSwitch was still the default engine. Removing SingleSwitch changes what they exercise, so update them to the MultiSwitch-only world: - test_control_lut_refresh.py / test_token_exchange.py: drop the ``"single"`` arm from the LUT-refresh parametrization; create_switch only ever returns MultiSwitch now, so the single arm asserted a type that can no longer be built. - test_granitemoe_compose_e2e.py: the switch reserves SWITCH_CACHE_LAYERS (== 2, MultiSwitch's counting + memory slots) at the front, not 1. Assert ``NUM_LAYERS + SWITCH_CACHE_LAYERS``; the old ``+ 1`` was the SingleSwitch single-slot layout. - test_multi_audio_compose_e2e.py: drop the ``--switch-type`` CLI flag (removed with SingleSwitch) and the ``single`` compose leg, and stop asserting a ``switch_type`` config key the composer no longer writes. The behavior each test pins (LUT refresh, front-loaded cache slots, audio compose shipping a consistent control LUT) is unchanged. Signed-off-by: noaa <noaa.kless@ibm.com>
The Vela run surfaced ~34 GPU/E2E failures the CPU suite never exercised, all from SingleSwitch assumptions left in tests after the engine was removed: - MultiSwitch reserves 2 cache slots (counting + memory), not 1. Fix the stale `num_hidden_layers - 1` in _model_forward_tests.py (its sibling at line 384 was already -2) and rewrite the KV-cache setup to register BOTH switch attention modules (counting_attn/memory_attn) instead of a single `.attn`. Size test_sr_switch's config to 4 layers (2 switch + 2 decoder) and drop the inert switch_type="single". - `single_overrides` was removed from generation_models; _noneager_generation_tests now uses basic_overrides (== switch_overrides, MultiSwitch). - Drop `config.switch_type` reads (the attribute no longer exists): the redundant asserts in test_multi_switch_alora/mixed_tech (both already isinstance-check MultiSwitch), the switch_type field emitted by the conversation workers, and the asserts consuming it. Build-phase anti-vacuity checks that read the raw config now key off the durable `ms_code_m` marker instead. Verified: test_sr_switch passes on CPU (25); all edited files clean under ruff 0.9.0 lint + format. The vLLM/integration ones re-verify on the next Vela run. Signed-off-by: noaa <noaa.kless@ibm.com>
…ingleSwitch preview test_quantization loaded ibm-granite/granite-switch-4.1-3b-preview, which the branch's from_dict now correctly rejects: that published checkpoint (like the 8b and 30b previews) is a legacy pre-coded-engine build — no ms_code_m, no switch_type — so it cannot load as MultiSwitch. No MultiSwitch checkpoint is published anywhere on the Hub. Follow the same pattern as test_multi_switch_mixed_tech: gate on GRANITE_SWITCH_E2E_MODELS=1 and compose (warm-reuse) a real ~3B mixed checkpoint from rag + guardian under GRANITE_SWITCH_E2E_DIR/multi-mixed. That yields exactly the two adapters ADAPTER_TESTS exercises — answerability (aLoRA, from rag) and hallucination_detection (LoRA, from guardian) — and reuses the identical checkpoint mixed_tech already composes, so no extra compose cost on a full run. GPU/E2E-only; verifies on the next Vela run. Signed-off-by: noaa <noaa.kless@ibm.com>
…h preview test_pipeline_parallelism_equivalence hardcoded ibm-granite/granite-switch-4.1-3b-preview, the same legacy SingleSwitch checkpoint from_dict now rejects — so both PP=1 and PP=2 worker steps died at config load. PP only needs a loadable MultiSwitch checkpoint with an active adapter to compare PP=1 vs PP=2 token equivalence, so gate on GRANITE_SWITCH_E2E_MODELS=1 and compose (warm-reuse) the same multi-mixed checkpoint test_quantization and test_multi_switch_mixed_tech build. GPU/E2E-only (needs 2 GPUs); verifies on the next Vela run. Signed-off-by: noaa <noaa.kless@ibm.com>
…ed adapters A GPU probe showed the 8 test_adapter_activates failures were neither a quantization nor a switch bug: the checkpoint the fixture composed did not contain answerability or hallucination_detection at all (its adapter_names were guardian- family: factuality-correction/-detection, guardian-core, policy-guardrails). So apply_chat_template(adapter_name="answerability") matched nothing, the control token was never injected (base and adapter prompts identical), and the adapter was a no-op in both bf16 and 4-bit — base == adapter, the assertion fails. granitelib-rag carries BOTH tested adapters (answerability as aLoRA, hallucination_detection as LoRA), so compose from rag alone into a dedicated "quant-rag" dir. The dedicated dir matters: the shared "multi-mixed" path may already hold a checkpoint composed from other libraries whose names would not match ADAPTER_TESTS. GPU/E2E-only; verifies on the next Vela run. Signed-off-by: noaa <noaa.kless@ibm.com>
….to() Removing SingleSwitch makes MultiSwitch the only (and default) engine, so every composed checkpoint now carries the fp32 Kerdock codebook buffer. The composer casts the model with model.to(bfloat16) (compose_utils.py), which nn.Module._apply would take the codebook down with it — but __init__ rebuilds the buffer as fp32 on every load. A cast model then writes a bf16 codebook while a reload of it writes fp32, so the same model serializes to two different byte counts (a 262,144-byte gap: 2048x64 fp32 vs bf16). Masked before the SingleSwitch removal because compose defaulted to the codebook-free SingleSwitch, so test_save_load_compose.py's byte-idempotency check (TestPhase2_DoubleSerialization::test_file_content_matches) never exercised the codebook path. With MultiSwitch as the default it does, and fails. _apply upcasts the codebook back to fp32 after any cast, restoring idempotency; the forward path is unaffected (codebook entries +/-0.125 are exact in bf16 and feed an fp32 dest). Verified: test_file_content_matches passes on CPU; tests/hf/test_multi_switch.py 297 pass. Signed-off-by: noaa <noaa.kless@ibm.com>
…rity Five branch-touched files had formatting the pinned ruff (0.9.0) reformats, and the aligned multi_turn_multiswitch notebook carried cell outputs. Apply ruff-format and nbstripout so `pre-commit run --all-files` (the CI source of truth) is clean. Signed-off-by: noaa <noaa.kless@ibm.com>
Remove session/branch cruft that does not help a reader understand the code: - MULTISWITCH_EXPLAINED.md: drop the "Converted from HTML on branch ... read at 1c86ee9 ... not been re-audited" provenance note, the "on this branch" framing, and the commit/branch archaeology for the alora-invocation-tail rule (kept the fact: the code uses the character rule, no alora_invocation_tail() helper here). - Drop "N bugs fixed on this branch" narrative from the multiswitch test docstrings, keeping the timeless rationale (each guards an end-to-end boundary). - refresh_switch_control_lut: drop the removed-SingleSwitch / --switch-type history, keep why it is extracted (testable without a real compose). - Retarget dangling references to the deleted SingleSwitch test test_switch_e2e_compose.py to the MultiSwitch serving e2e tests / generic notes. Signed-off-by: noaa <noaa.kless@ibm.com>
noaakl
requested review from
antonpibm,
aviv1ron1,
freunda and
yairallouche
as code owners
September 9, 2026 13:04
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Collaborator
|
/gpu-test-multi |
✅ GPU tests passed —
|
✅ GPU tests passed —
|
yairallouche
approved these changes
Sep 10, 2026
aviv1ron1
added a commit
that referenced
this pull request
Sep 10, 2026
…ture #131 removed SingleSwitch. test_granitemoe_audio_compose.py arrived from feature/moe-audio-support, which branched before that, so it still parametrized its fixture over ["single", "multi"] and asserted the switch class was SingleSwitch on the single arm -- 9 errors after the merge, every one a [single] case, while all 23 [multi] cases passed. Now SWITCH_TYPES = ["multi"] and the assertion is unconditional, which is what every other switch-parametrized file already does post-#131: test_control_lut_refresh.py:34, tests/hf/test_multi_switch.py:49, tests/vllm/test_multi_switch.py:52, tests/shared/gap_equivalence.py:15. So this converges on the established shape rather than inventing one. Nothing about the audio path changed -- the coverage that was single/multi parametrized is engine-independent (the marker's embedding and output rows, the control-LUT refresh, save/load survival), so dropping the removed engine loses no assertion. 23 passed. The wider audio tier is 128 passed / 53 skipped with one error that is this machine having no GPU ("Torch not compiled with CUDA enabled"), not a merge regression. Signed-off-by: aviv ron <rona@il.ibm.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.