Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7e872cc
Automate the vLLM port so the recurring cost is review, not labour
ItzikVa Oct 17, 2026
0c7b55f
Check the uv pin resolves, not which uv happens to be installed
ItzikVa Oct 18, 2026
acdfb67
Make the four scored evals data instead of four 34 KB YAMLs
ItzikVa Oct 18, 2026
129862f
State the skill's contract, and prove each added version separately
ItzikVa Oct 19, 2026
e7b0f6d
Make the tier-1 adapter evals runnable on the GPU cluster
ItzikVa Oct 19, 2026
888947a
Get per-line vLLM evidence out of a two-leg GPU workflow
ItzikVa Oct 19, 2026
33c7813
Stop the skill from checking out a main that would delete it
ItzikVa Oct 19, 2026
384b4d8
Make preflight check the workflow that actually runs, and the eval ba…
ItzikVa Oct 19, 2026
90a84e8
Refuse a green GPU run that skipped every test
ItzikVa Oct 20, 2026
47c5899
Let the eval suite obtain its own checkpoint instead of skipping forever
ItzikVa Oct 20, 2026
edaae7d
Record baselines with the same sampler the check uses
ItzikVa Oct 20, 2026
8dbf058
Commit the tier-1 eval baselines, recorded on vLLM 0.30
ItzikVa Oct 20, 2026
f22db19
Measure the eval gate teacher-forced instead of over a greedy rollout
ItzikVa Oct 20, 2026
b8b9c00
Re-record the tier-1 eval baselines teacher-forced
ItzikVa Oct 21, 2026
5e2ee66
Disable prefix caching while capturing eval distributions
ItzikVa Oct 21, 2026
0cc6cbf
Re-record the vLLM eval baselines with prefix caching off
ItzikVa Oct 21, 2026
7224bbe
Pin the eval's adapter by name: compose order is filesystem order
ItzikVa Oct 21, 2026
06e2140
Record eval baselines on the runner that checks them
ItzikVa Oct 21, 2026
6277cd4
Re-record the eval baselines from the GPU runner's own 0.30 leg
ItzikVa Oct 21, 2026
4b60198
Stop the eval metric from measuring its own capture window
ItzikVa Oct 21, 2026
06f387e
Teach the skill the eval gate's two invariants and where to record
ItzikVa Oct 21, 2026
764e2cd
State the drift metric's semantics in the PR body
ItzikVa Oct 21, 2026
72edb07
Explain the upgrade-vllm pipeline in one page
ItzikVa Oct 22, 2026
5201e38
Fix the upstream API diff reporting every symbol as unchanged
ItzikVa Oct 22, 2026
23d2c8d
Gate the ASR processor on the multimodal hook's shape, not its existence
ItzikVa Oct 22, 2026
d08fa78
Declare vLLM 0.31 and 0.32, with dev-latest parked on 0.31
ItzikVa Oct 22, 2026
eb83c33
Walk dev-latest to 0.32, the merge-ready state
ItzikVa Oct 22, 2026
d3ecc6b
Read the 0.31+ multimodal data under the key upstream actually passes
ItzikVa Oct 23, 2026
1d86469
Call the processor hooks the way each supported vLLM line calls them
ItzikVa Oct 23, 2026
10ba7ae
Re-park dev-latest on 0.31 to re-evidence it with the audio fixes
ItzikVa Oct 23, 2026
2040e81
Walk dev-latest back to 0.32, the merge-ready state
ItzikVa Oct 23, 2026
bc16035
Pass the cascade-attention metadata fields only where they exist
ItzikVa Oct 25, 2026
7312f4f
Assert the CTC transcript on its words, not on its orthography
ItzikVa Oct 25, 2026
806aced
Re-park dev-latest on 0.31 to re-evidence it with the cascade fix
ItzikVa Oct 25, 2026
c841131
Walk dev-latest back to 0.32, the merge-ready state
ItzikVa Oct 25, 2026
2baba46
Move the upgrade-vllm skill out of the public repo
ItzikVa Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions .github/workflows/gpu-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ on:
# family is added HERE and nowhere else -- this entry plus its arm in the
# Resolve test scope step below.
suite:
description: "Which tests to run: full (all five suites), short (vllm + integration), dev (one fast GPU file), audio (the audio/ASR marker), multi (all five on 2 GPUs)."
description: "Which tests to run: full (all five suites), short (vllm + integration), dev (one fast GPU file), audio (the audio/ASR marker), multi (all five on 2 GPUs), eval (the four adapter evals vs committed baselines), eval-record (re-record those baselines; prints them, commits nothing)."
required: false
default: full
type: choice
options: [full, short, dev, audio, multi]
options: [full, short, dev, audio, multi, eval, eval-record]
# RESULT CACHE OPT-OUT. Default off, i.e. cache on: the expensive thing here is
# a GPU run, and re-running one for a commit+scope that was already tested buys
# nothing in the common case (a reviewer re-reading a PR, a second maintainer
Expand Down Expand Up @@ -257,6 +257,41 @@ jobs:
LEG="$LABEL-multi"
GPU_COUNT="2"
;;
# The adapter evals: tier-1 drift checks that compare this commit's
# adapter output against the committed baselines in
# tests/eval/baselines/. Selected by MARKER and restricted to
# tests/eval/ -- every other scope excludes them, because `eval` is
# not in pyproject's default `-m "not deep"` selection and the files
# need adapter checkpoints staged on the pod.
#
# "and not deep" for the same reason as `audio`: -m on the command
# line REPLACES addopts' `-m "not deep"` instead of intersecting with
# it, so a bare `-m eval` would also pull in the deep eval cases.
eval)
PATHS='-m "eval and not deep" tests/eval/'
SUFFIX="eval"
LEG="$LABEL-eval"
;;
# RECORDS the baselines the `eval` scope checks against, printing each
# into the log for a human to extract and commit. Writes nothing to the
# repo -- see tests/eval/test_record_baselines.py.
#
# Recording happens HERE, on the runner that does the checking, because
# a baseline recorded elsewhere was not reproducible here: a Vela-
# recorded one missed by a systematic ~0.004 mean JSD on both vLLM
# configs, identically on the 0.26 and 0.30 legs. Same-environment
# recording makes the baseline reproducible by construction, so later
# movement is attributable to the vLLM line and nothing else.
#
# `-m eval_record` with NO "and not deep" -- the opposite of every arm
# above, on purpose. These cases are marked `deep` precisely so the
# default selection cannot reach them, so intersecting with "not deep"
# would select nothing and the leg would pass vacuously.
eval-record)
PATHS='-m eval_record tests/eval/'
SUFFIX="eval-record"
LEG="$LABEL-eval-record"
;;
# Fail rather than defaulting to full: silently running the wrong scope
# is worse than not running, and `type: choice` already rejects unknown
# values, so reaching this means the options list above gained an entry
Expand Down Expand Up @@ -310,6 +345,9 @@ jobs:
# the opposite of what it reads like.
NO_CACHE: ${{ inputs.no_cache }}
DEP_GROUP: ${{ matrix.dep_group }}
# Needed for the eval-record bypass below. SUITE in the mapping step is
# that step's own env and does not carry across steps.
SUITE: ${{ inputs.suite || 'full' }}
# No `set -e`: every failure path here is a cache miss, not a broken run.
run: |
set -uo pipefail
Expand All @@ -331,7 +369,14 @@ jobs:
awk -v k="$1" 'index($0, k "=") == 1 { print substr($0, length(k) + 2); exit }' <<<"$LOOKUP"
}

if [[ "$NO_CACHE" == "true" ]]; then
# eval-record is never cacheable. Its whole output is the baseline JSON
# printed into the pod log, and a cache hit skips the pod entirely — so a
# second recording dispatch on the same commit would report success with
# nothing to extract, and the obvious reading of that is "the recording
# produced no baselines".
if [[ "$SUITE" == "eval-record" ]]; then
echo "eval-record always runs: the log IS the artifact, so a cached result is useless."
elif [[ "$NO_CACHE" == "true" ]]; then
echo "--no-cache was requested — running the tests fresh."
else
# stderr goes to a file, never to this console: on a failure the script
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ pip install "granite-switch[dev]" # Everything

Requires Python 3.11+ and PyTorch 2.11+.

> **vLLM version note:** `[vllm]` accepts any line from `0.26.x` to `0.30.x`. The floor is
> **vLLM version note:** `[vllm]` accepts any line from `0.26.x` to `0.32.x`. The floor is
> `transformers>=5.16`: `0.26` is the earliest vLLM whose model code handles the
> transformers-5.13+ `full_attention` layer-type rename. One tree serves all five lines —
> transformers-5.13+ `full_attention` layer-type rename. One tree serves all seven lines —
> where their APIs differ, the backend checks for the capability rather than the version. All
> of them require PyTorch 2.11+ (CUDA 13+). To pin a single line for development, use the
> `vllm26` / `vllm30` dependency *groups* — the two ends of the range, which is also what CI
> `vllm26` / `vllm32` dependency *groups* — the two ends of the range, which is also what CI
> tests on every GPU run.

### Compose a Model
Expand Down
3 changes: 2 additions & 1 deletion docs/AUDIO.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ checkpoint at any `--asr-model` your installed transformers supports (for exampl

```bash
# Serving an audio-enabled checkpoint
uv sync --extra vllm --extra audio # any vLLM 0.26.x - 0.30.x
uv sync --extra vllm --extra audio # any vLLM 0.26.x - 0.32.x

# Development / running the test suite (the dev groups include audio already)
uv sync --group dev # vLLM 0.26.x
uv sync --group dev-vllm27 # vLLM 0.27.x
uv sync --group dev-vllm32 # vLLM 0.32.x
```

## Building an audio-enabled checkpoint
Expand Down
8 changes: 4 additions & 4 deletions docs/SWITCH_KERNELS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ rank-32 ones, and **up to 5.9× faster** with one rank-512 adapter. At idle (no
## Motivation and Availability

As part of our mission to create a modular model architecture where adapter libraries are used to enhance
a model, we encountered the problem that existing infrastructure for serving adapters introduced
unnecessary performance overheads. These overheads are noticeable even when a single adapter, regardless of
a model, we encountered the problem that existing infrastructure for serving adapters introduced
unnecessary performance overheads. These overheads are noticeable even when a single adapter, regardless of
its size, is introduced, but become very pronounced when the adapters have diverse sizes, which we anticipate will be a common
scenario when a developer is integrating library components from different contributors into a single checkpoint.

Expand All @@ -36,7 +36,7 @@ expensive in our setting.
## What SWITCH does differently

SWITCH is named after its design: **S**witched delta, **W**-ext fusion, **I**n-place,
**T**iled, **C**onditional skip, **H**eterogeneous rank.
**T**iled, **C**onditional skip, **H**eterogeneous rank.

Existing Punica technology, as implemented in vLLM, exhibits the following properties:

Expand Down Expand Up @@ -108,7 +108,7 @@ adapter. The figure below compares each system **as it currently implemented**:

![Idle vs adapter-active throughput at 12 adapters loaded, against the base model with LoRA off](figures/fig3_dispatch_3b_b32.png)

Of note is that the overhead at idle that the SWITCH implementation in Granite Switch and vLLM exhibit is unrelated to the SWITCH kernels themselves, and rather
Of note is that the overhead at idle that the SWITCH implementation in Granite Switch and vLLM exhibit is unrelated to the SWITCH kernels themselves, and rather
related to the fact that SWITCH is operating as a single computational graph, whereas vLLM does not run an adapter specific graph when no adapters are present. It would be relatively
straightforward to implement SWITCH with two graphs, in which case the performance in the no adapter case would match vLLM/Punica exactly. As indicated earlier, we are currently focused in the setting where the model discovers which adapters need to be applied to which tokens through its switch layer, and currently there is no support in vLLM for adapting the computational graph once the model is already invoked.

Expand Down
121 changes: 93 additions & 28 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ Documentation = "https://github.com/generative-computing/granite-switch/tree/mai

[project.optional-dependencies]
hf = ["accelerate>=0.20.0"]
# Every line from 0.26 through 0.30 is supported by one tree; the backend uses
# capability checks (hasattr) rather than version comparisons where the two
# differ. Pin a specific line with the per-line dependency groups below.
vllm = ["vllm>=0.26.0,<0.31.0"]
# Every line from 0.26 through 0.32 is supported by one tree; the backend uses
# capability checks rather than version comparisons where the two differ. Prefer
# probing an API's SHAPE over its mere existence: 0.31 reintroduced the
# multimodal `_call_hf_processor` hook that 0.29 had deleted, but with a
# different signature, so `hasattr` alone reads 0.26-0.28 and 0.31+ as the same
# thing (see src/granite_switch/vllm/audio/processor.py).
# Pin a specific line with the per-line dependency groups below.
vllm = ["vllm>=0.26.0,<0.33.0"]
compose = ["huggingface_hub", "pyyaml", "tqdm", "safetensors"]
build = ["huggingface_hub", "pyyaml", "tqdm", "safetensors"] # Backward compatibility alias for compose
# Audio (ASR) decode + resample. Reuse vLLM's own audio deps (unversioned, so it
Expand Down Expand Up @@ -55,17 +59,25 @@ markers = [
"deep: expensive code-theory tests (m=8 / 256-dim); run with: pytest -m deep",
"requires_model: needs a real model checkpoint",
"audio: exercises the audio/ASR path; run with: pytest -m audio",
"eval: adapter-output eval against a committed baseline; run with: pytest -m eval",
# Also marked `deep`, so the default `-m "not deep"` hides it: this one RECORDS
# the baselines the `eval` marker checks against, and must only ever run when
# asked for by name.
"eval_record: records eval baselines on the GPU runner; run with: pytest -m eval_record",
]

[dependency-groups]
# vllm26 AND vllm30 ARE THE TWO CI LEGS -- the ends of the supported range, which
# is why these two and not two others. The `vllm` extra above claims 0.26-0.30,
# and the two bounds are where a break actually lands: a deleted API shows up at
# the ceiling and an API that does not exist yet shows up at the floor.
# 0.27/0.28/0.29 sit strictly between two tested lines, so they add install time
# without adding a failure mode the ends would miss. All five lines were verified
# together out of band (five-leg GPU matrix, every suite green); these two are
# what CI re-checks on every run.
# vllm26 AND whatever `dev-latest` points at ARE THE TWO CI LEGS -- the ends of
# the supported range, which is why these and not two others. The `vllm` extra
# above claims 0.26-0.32, and the two bounds are where a break actually lands: a
# deleted API shows up at the ceiling and an API that does not exist yet shows up
# at the floor. The intermediate lines sit strictly between two tested lines, so
# per-run they would add install time without adding a failure mode the ends
# would miss -- but they are NOT taken on trust: each added line gets its own
# commit that repoints `dev-latest`, dispatched at that commit's sha, because
# derive_legs.py reads the tested commit and emits at most two legs. The walk
# itself is automated by the `upgrade-vllm` skill, which lives in the internal
# repo (generative-computing/granite-switch-internal, .claude/skills/upgrade-vllm/).
vllm26 = ["vllm>=0.26.0,<0.27.0"]
vllm30 = ["vllm>=0.30.0,<0.31.0"]
# vllm27 IS NOT A CI LEG -- it exists so the GPU workflow stays dispatchable
Expand All @@ -75,55 +87,108 @@ vllm30 = ["vllm>=0.30.0,<0.31.0"]
# dev-vllm27) while checking out the PR's commit. Deleting dev-vllm27 here made
# that leg unresolvable against this tree -- `uv sync --group dev-vllm27` has no
# such group -- and the leg died before pytest started. Keeping the group costs
# nothing and is not a fiction: the `vllm` extra claims 0.26-0.30, so 0.27 is a
# nothing and is not a fiction: the `vllm` extra claims 0.26-0.32, so 0.27 is a
# supported line that simply isn't worth its own per-run leg. Removable once the
# matrix rename is on main.
vllm27 = ["vllm>=0.27.0,<0.28.0"]
vllm31 = ["vllm>=0.31.0,<0.32.0"]
vllm32 = ["vllm>=0.32.0,<0.33.0"]
# `audio` is included so the audio tests can actually run: the ASR path needs
# vLLM's audio deps (av/soundfile/resampy) at runtime, and no group pulled them
# in before (integration tests failed with ModuleNotFoundError on a synced pod).
dev = ["pytest", "pytest-cov", { include-group = "vllm26" }, "granite-switch[hf,compose,audio]"]
dev-vllm27 = ["pytest", "pytest-cov", { include-group = "vllm27" }, "granite-switch[hf,compose,audio]"]
dev-vllm30 = ["pytest", "pytest-cov", { include-group = "vllm30" }, "granite-switch[hf,compose,audio]"]
dev-vllm31 = ["pytest", "pytest-cov", { include-group = "vllm31" }, "granite-switch[hf,compose,audio]"]
dev-vllm32 = ["pytest", "pytest-cov", { include-group = "vllm32" }, "granite-switch[hf,compose,audio]"]
# The newest vLLM line, as an alias. The GPU workflow (derive_legs.py) and
# CLAUDE.md ask for `dev-latest` rather than the concrete `dev-vllmNN`, so moving
# the ceiling is repointing this one line instead of chasing the version through
# every caller. Currently the 0.30 line.
dev-latest = [{ include-group = "dev-vllm30" }]
# every caller. Now on 0.32, the true ceiling of the `vllm` extra's range --
# which is also the only state that may merge, since CI's upper leg comes from
# this alias and anything lower would leave the top supported line untested
# (the internal skill's `plumb_pyproject.py --check --final` enforces exactly
# that). The preceding commit parked it on 0.31 to collect that line's own GPU
# evidence at its own sha, because derive_legs.py reads the TESTED commit rather
# than the branch tip.
dev-latest = [{ include-group = "dev-vllm32" }]
test = ["pytest", "pytest-cov", "bitsandbytes", "optimum-quanto", { include-group = "dev" }]

[tool.uv]
default-groups = ["vllm26"]
# Only the per-line GROUPS are mutually exclusive now. The `vllm` extra spans
# 0.26-0.30, so it overlaps every group and no longer conflicts with any of them --
# which is what made `uv sync` unsatisfiable while the extra said 0.30-only and the
# default group said 0.26.
# Every pair that would pull two different vLLM lines into one venv. Three lines
# are declared (0.26 / 0.27 / 0.30) with a bare and a dev- flavour each, so the
# 26-side {vllm26, dev}, the 27-side {vllm27, dev-vllm27} and the 30-side
# {vllm30, dev-vllm30} give 3 x (2 x 2) = 12 cross-line pairs. `test` is absent
# deliberately: it reaches vllm26 through `include-group = "dev"` and uv derives
# that transitively -- main's two-line version listed it nowhere either.
# GENERATED by `plumb_pyproject.py` in the internal repo's upgrade-vllm skill
# (generative-computing/granite-switch-internal). Edit by hand only if you also
# regenerate the whole table -- see the quadratic-growth note below.
# Only the per-line GROUPS are mutually exclusive. The `vllm` extra spans
# 0.26-0.32, so it overlaps every group and conflicts with none of them --
# an extra pinned to one line while default-groups named another is what made
# `uv sync` unsatisfiable once before.
# Below: every pair of groups that would pull two different vLLM lines into one
# venv. 5 lines are declared (0.26 / 0.27 / 0.30 / 0.31 / 0.32), each with a bare and a
# dev flavour, so the 10 cross-line pairs give 48 entries. The line
# `dev-latest` aliases carries a third name and so appears in more pairs --
# uv matches group names literally, so the alias needs its own entries.
# `test` is absent deliberately: it reaches the floor through
# `include-group = "dev"` and uv derives that transitively.
conflicts = [
# 0.26 vs 0.27
[{ group = "vllm26" }, { group = "vllm27" }],
[{ group = "vllm26" }, { group = "dev-vllm27" }],
[{ group = "dev" }, { group = "vllm27" }],
[{ group = "dev" }, { group = "dev-vllm27" }],
# 0.26 vs 0.30 (dev-latest aliases dev-vllm30, so it conflicts with the 0.26 side too)
# 0.26 vs 0.30
[{ group = "vllm26" }, { group = "vllm30" }],
[{ group = "vllm26" }, { group = "dev-vllm30" }],
[{ group = "vllm26" }, { group = "dev-latest" }],
[{ group = "dev" }, { group = "vllm30" }],
[{ group = "dev" }, { group = "dev-vllm30" }],
# 0.26 vs 0.31
[{ group = "vllm26" }, { group = "vllm31" }],
[{ group = "vllm26" }, { group = "dev-vllm31" }],
[{ group = "dev" }, { group = "vllm31" }],
[{ group = "dev" }, { group = "dev-vllm31" }],
# 0.26 vs 0.32
[{ group = "vllm26" }, { group = "vllm32" }],
[{ group = "vllm26" }, { group = "dev-vllm32" }],
[{ group = "vllm26" }, { group = "dev-latest" }],
[{ group = "dev" }, { group = "vllm32" }],
[{ group = "dev" }, { group = "dev-vllm32" }],
[{ group = "dev" }, { group = "dev-latest" }],
# 0.27 vs 0.30
[{ group = "vllm27" }, { group = "vllm30" }],
[{ group = "vllm27" }, { group = "dev-vllm30" }],
[{ group = "vllm27" }, { group = "dev-latest" }],
[{ group = "dev-vllm27" }, { group = "vllm30" }],
[{ group = "dev-vllm27" }, { group = "dev-vllm30" }],
# 0.27 vs 0.31
[{ group = "vllm27" }, { group = "vllm31" }],
[{ group = "vllm27" }, { group = "dev-vllm31" }],
[{ group = "dev-vllm27" }, { group = "vllm31" }],
[{ group = "dev-vllm27" }, { group = "dev-vllm31" }],
# 0.27 vs 0.32
[{ group = "vllm27" }, { group = "vllm32" }],
[{ group = "vllm27" }, { group = "dev-vllm32" }],
[{ group = "vllm27" }, { group = "dev-latest" }],
[{ group = "dev-vllm27" }, { group = "vllm32" }],
[{ group = "dev-vllm27" }, { group = "dev-vllm32" }],
[{ group = "dev-vllm27" }, { group = "dev-latest" }],
# 0.30 vs 0.31
[{ group = "vllm30" }, { group = "vllm31" }],
[{ group = "vllm30" }, { group = "dev-vllm31" }],
[{ group = "dev-vllm30" }, { group = "vllm31" }],
[{ group = "dev-vllm30" }, { group = "dev-vllm31" }],
# 0.30 vs 0.32
[{ group = "vllm30" }, { group = "vllm32" }],
[{ group = "vllm30" }, { group = "dev-vllm32" }],
[{ group = "vllm30" }, { group = "dev-latest" }],
[{ group = "dev-vllm30" }, { group = "vllm32" }],
[{ group = "dev-vllm30" }, { group = "dev-vllm32" }],
[{ group = "dev-vllm30" }, { group = "dev-latest" }],
# 0.31 vs 0.32
[{ group = "vllm31" }, { group = "vllm32" }],
[{ group = "vllm31" }, { group = "dev-vllm32" }],
[{ group = "vllm31" }, { group = "dev-latest" }],
[{ group = "dev-vllm31" }, { group = "vllm32" }],
[{ group = "dev-vllm31" }, { group = "dev-vllm32" }],
[{ group = "dev-vllm31" }, { group = "dev-latest" }],
]

[tool.coverage.run]
Expand Down
Loading
Loading