[Perf] Add reusable exact projection caching with MiniMax-H3 integration (#7987)
Signed-off-by: lishunyang12 <lishunyang12@users.noreply.github.com> Signed-off-by: Armaan Amatya <armaanamatya2014@gmail.com> Co-authored-by: lishunyang12 <lishunyang12@users.noreply.github.com> Co-authored-by: Armaan Amatya <38102001+armaanamatya@users.noreply.github.com>
This commit is contained in:
11 files changed
+1886
-3
No files matched your search
@@ -57,3 +57,58 @@ cross-stage routing.
|
|||||||
|
|
||||||
Test registry selection, checkpoint loading, minimal inference, input and output
|
Test registry selection, checkpoint loading, minimal inference, input and output
|
||||||
contracts, and every declared optional capability.
|
contracts, and every declared optional capability.
|
||||||
|
|
||||||
|
## Exact conditioning projection reuse
|
||||||
|
|
||||||
|
Models with repeated conditioning projections can use `ExactProjectionCache`
|
||||||
|
from `vllm_omni.diffusion.cache`. It stores actual projection outputs for identical
|
||||||
|
conditioning tensors and unchanged weights, with a byte budget and TP-wide hit
|
||||||
|
coordination. It has no model names, sampling schedules, projection layouts, or
|
||||||
|
artifact formats built in. MiniMax-H3 is its first model integration.
|
||||||
|
|
||||||
|
Keep one cache per model instance and prepare each conditioning tensor once
|
||||||
|
before its projections. For a leaf linear with fixed SiLU preprocessing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from torch.nn.functional import silu
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.cache import ExactProjectionCache
|
||||||
|
|
||||||
|
cache = ExactProjectionCache(max_bytes=256 * 1024**2)
|
||||||
|
|
||||||
|
# Inside an inference forward, with gradients disabled:
|
||||||
|
cache.prepare(conditioning)
|
||||||
|
modulation = cache.project(
|
||||||
|
"block.0.modulation",
|
||||||
|
projection,
|
||||||
|
conditioning,
|
||||||
|
lambda: projection(silu(conditioning)),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The original computation must depend only on the supplied conditioning tensor,
|
||||||
|
the leaf projection's weights/buffers, and fixed preprocessing. Include prompt,
|
||||||
|
guidance, modality, or other request-dependent conditioning in the actual input;
|
||||||
|
a timestep-only key is unsafe when those affect the result. Do not mutate the
|
||||||
|
input between `prepare` and `project`, or modify outputs returned on cache hits.
|
||||||
|
Use separate cache instances for concurrent forwards with different inputs.
|
||||||
|
|
||||||
|
Call `clear()` when loading or switching adapters, changing projection behavior,
|
||||||
|
or moving the model. Weight-version checks invalidate individual entries.
|
||||||
|
Compilation, gradient-enabled execution, projection hooks, and unversioned
|
||||||
|
inference weights use the original computation. Validate cold/warm parity,
|
||||||
|
invalidation, and rank-local misses when integrating another model.
|
||||||
|
Hook checks include global hooks and hooks on wrapped submodules. Numerical
|
||||||
|
settings are captured at each projection call, so entering an autocast context
|
||||||
|
after `prepare()` cannot reuse an output from a different precision.
|
||||||
|
|
||||||
|
Validated offline results can be supplied through `_lookup_precomputed()`;
|
||||||
|
this extension is consulted only at TP1 on runtime-cache misses. Artifact
|
||||||
|
validation, weight binding, and model-specific layouts remain in the adapter.
|
||||||
|
H3's adapter retains its main/Ref2VA sidecars and their eager-only admission.
|
||||||
|
|
||||||
|
The cache retains projection results and leaves all original weights available.
|
||||||
|
It does not reduce weight residency or cancel layerwise prefetch. A future
|
||||||
|
integration that skips weight transfers must coordinate with `OffloadPlan`
|
||||||
|
before prefetch and provide a safe miss path; moving weight storage currently
|
||||||
|
causes conservative cache invalidation.
|
||||||
@@ -1017,6 +1017,84 @@ balanced switch order.
|
|||||||
> workload. The values above apply to this deployment and are not universal
|
> workload. The values above apply to this deployment and are not universal
|
||||||
> guarantees. `lossless` remains the exact reference path.
|
> guarantees. `lossless` remains the exact reference path.
|
||||||
|
|
||||||
|
## Exact AdaLN reuse
|
||||||
|
|
||||||
|
H3 enables exact AdaLN projection reuse by default, independently of the task,
|
||||||
|
hardware, sampling schedule, quantization setting, and request `quality`.
|
||||||
|
This applies to both original H3 (including its default 50-point schedule) and
|
||||||
|
FastH3; no distilled adapter or explicit few-step ladder is required.
|
||||||
|
The first occurrence of an input runs the existing projection; later identical
|
||||||
|
time embeddings reuse its output only while the projection weights and numerical
|
||||||
|
settings remain unchanged. This does not approximate neighboring timesteps or
|
||||||
|
change attention, precision, sampling, or the generated audio/video contract.
|
||||||
|
|
||||||
|
Each DiT keeps at most 256 MiB of runtime projection outputs. If a long schedule
|
||||||
|
exceeds that budget, it retains a reusable subset and computes other entries
|
||||||
|
normally, avoiding cyclic eviction on repeated original-H3 requests. All original weights
|
||||||
|
remain loaded so new schedules and adapters can compute normally. Adapter changes,
|
||||||
|
weight reloads, and model device moves invalidate the cache; parameter versions
|
||||||
|
also guard individual entries. TP ranks coordinate hits before skipping a
|
||||||
|
projection collective. Gradient-enabled execution, compilation, custom linear
|
||||||
|
hooks, and tensors without weight version counters use the original computation.
|
||||||
|
Offload paths that replace weight storage can therefore reduce the hit rate.
|
||||||
|
|
||||||
|
The runtime cache uses the shared `ExactProjectionCache` implementation; H3 keeps
|
||||||
|
only its optional sidecar adaptation. Other models can integrate the same
|
||||||
|
[projection cache interface](../../docs/design/module/diffusion/diffusion_model_integration.md#exact-conditioning-projection-reuse).
|
||||||
|
This cache retains projection outputs, not offloaded weights, and does not skip
|
||||||
|
block weight prefetch.
|
||||||
|
|
||||||
|
For an A/B comparison, disable only this reuse at server startup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--cache-config '{"minimax_h3_adaln_cache": false}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional offline sidecar
|
||||||
|
|
||||||
|
An offline sidecar can seed the first projection results; it is not required to
|
||||||
|
enable the default cache. Sidecars require `--enforce-eager`, native BF16 TP1
|
||||||
|
math, and the same numerical environment as the builder. With the default
|
||||||
|
compiled execution, sidecars are rejected before reading their payloads: compiled
|
||||||
|
H3 blocks bypass cached projections, so retaining those payloads would waste GPU
|
||||||
|
memory. This applies to both the main and Ref2VA sidecars. Other serving
|
||||||
|
configurations retain the default runtime-cache behavior described above.
|
||||||
|
From the repository root, for a fixed FastH3 adapter and its own four-step schedule:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=. python tools/minimax_h3/build_adaln_cache.py \
|
||||||
|
--transformer-path "${MODEL_ROOT}/FL2VA/transformer" \
|
||||||
|
--model-variant fl2va --mode t2va \
|
||||||
|
--fasth3-adapter "${FASTH3_ADAPTER}" \
|
||||||
|
--num-inference-steps 4 --flow-shift 12 --audio-flow-shift 3 \
|
||||||
|
--device cuda --output /path/to/h3-adaln.safetensors
|
||||||
|
```
|
||||||
|
|
||||||
|
Omit `--fasth3-adapter` for base weights and use the serving step count and shifts.
|
||||||
|
The builder accepts a native transformer directory with `config.json` and indexed
|
||||||
|
or single-file safetensors. It streams the required inputs and refuses to overwrite
|
||||||
|
an existing output. It does not instantiate the full DiT.
|
||||||
|
|
||||||
|
Pass the resulting local artifact at eager server startup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--enforce-eager \
|
||||||
|
--cache-config '{"minimax_h3_adaln_cache_path": "/path/to/h3-adaln.safetensors"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Combined servers can also use `minimax_h3_ref_adaln_cache_path` for their Ref2VA
|
||||||
|
transformer. Each artifact binds the model architecture, task, schedule, shifts,
|
||||||
|
fixed adapter file, effective time-embedding/AdaLN weights, payload checksums, and
|
||||||
|
numerical environment. A missing, corrupt, incompatible, or outdated sidecar is
|
||||||
|
rejected with a warning; the model still loads every weight and computes through
|
||||||
|
the runtime path. A request with different settings similarly falls back.
|
||||||
|
Build sidecars from trusted local inputs: checksums verify identity and integrity,
|
||||||
|
not the correctness of an untrusted generator.
|
||||||
|
|
||||||
|
This implementation saves repeated projection work. It does not remove AdaLN
|
||||||
|
weights or claim a GPU memory reduction. End-to-end gains depend on the workload,
|
||||||
|
offload behavior, embedding fingerprint cost, and TP coordination overhead.
|
||||||
|
|
||||||
## LoRA
|
## LoRA
|
||||||
|
|
||||||
### Turbo LoRA
|
### Turbo LoRA
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
"""Exact projection reuse without model-specific schedules or artifacts."""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.cache.exact_projection_cache import ExactProjectionCache
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def tp1(monkeypatch):
|
||||||
|
import vllm.distributed
|
||||||
|
|
||||||
|
monkeypatch.setattr(vllm.distributed, "get_tensor_model_parallel_world_size", lambda: 1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def deterministic_weights():
|
||||||
|
with torch.random.fork_rng(devices=[]):
|
||||||
|
torch.manual_seed(0)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("batch,in_features,out_features", [(2, 4, 8), (1, 8, 48), (4, 6, 18)])
|
||||||
|
def test_runtime_reuses_exact_outputs_and_tracks_changed_weights(mocker, batch, in_features, out_features):
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(in_features, out_features)
|
||||||
|
x = torch.randn(batch, in_features)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
with torch.no_grad():
|
||||||
|
cache.prepare(x)
|
||||||
|
first = cache.project("block", linear, x, compute)
|
||||||
|
first_copy = first.clone()
|
||||||
|
first.zero_() # The caller/provider cannot corrupt the owned cache.
|
||||||
|
assert torch.equal(cache.project("block", linear, x, compute), first_copy)
|
||||||
|
assert compute.call_count == 1
|
||||||
|
linear.weight.add_(0.25)
|
||||||
|
assert torch.equal(cache.project("block", linear, x, compute), linear(x))
|
||||||
|
assert compute.call_count == 2
|
||||||
|
other = x.clone()
|
||||||
|
other[0, 0] += 1
|
||||||
|
cache.prepare(other)
|
||||||
|
cache.project("block", linear, other, lambda: linear(other))
|
||||||
|
cache.prepare(x)
|
||||||
|
cache.project("block", linear, x, compute)
|
||||||
|
assert compute.call_count == 2
|
||||||
|
assert cache.hits == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_budget_retains_reusable_entries_without_schedule_thrashing(mocker):
|
||||||
|
cache = ExactProjectionCache(max_bytes=32)
|
||||||
|
linear = torch.nn.Linear(2, 4)
|
||||||
|
x = torch.ones(2, 2)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
with torch.no_grad():
|
||||||
|
cache.prepare(x)
|
||||||
|
for name in ("first", "second", "first"):
|
||||||
|
cache.project(name, linear, x, compute)
|
||||||
|
assert cache._bytes <= 32
|
||||||
|
assert compute.call_count == 2
|
||||||
|
assert cache.hits == 1
|
||||||
|
cache.clear()
|
||||||
|
assert cache._bytes == 0 and not cache._entries
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_lora_buffers_and_suspend_mask_invalidate_entries(mocker):
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
linear.lora_a_stacked = (torch.ones(1, 2),)
|
||||||
|
linear.lora_b_stacked = (torch.ones(2, 1),)
|
||||||
|
linear._diffusion_lora_active_slices = (True,)
|
||||||
|
x = torch.ones(1, 2)
|
||||||
|
|
||||||
|
def compute():
|
||||||
|
result = linear(x)
|
||||||
|
if linear._diffusion_lora_active_slices[0]:
|
||||||
|
result = result + x @ linear.lora_a_stacked[0].T @ linear.lora_b_stacked[0].T
|
||||||
|
return result
|
||||||
|
|
||||||
|
counted = mocker.Mock(side_effect=compute)
|
||||||
|
with torch.no_grad():
|
||||||
|
cache.prepare(x)
|
||||||
|
cache.project("a", linear, x, counted)
|
||||||
|
cache.project("a", linear, x, counted)
|
||||||
|
assert counted.call_count == 1
|
||||||
|
linear.lora_b_stacked[0].add_(1)
|
||||||
|
assert torch.equal(cache.project("a", linear, x, counted), compute())
|
||||||
|
linear._diffusion_lora_active_slices = (False,)
|
||||||
|
assert torch.equal(cache.project("a", linear, x, counted), compute())
|
||||||
|
assert counted.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("scope", ["local", "global", "child"])
|
||||||
|
@pytest.mark.parametrize("kind", ["pre", "post"])
|
||||||
|
def test_linear_hooks_run_even_after_cache_warmup(scope, kind):
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
projection = torch.nn.Sequential(linear) if scope == "child" else linear
|
||||||
|
x = torch.ones(1, 2)
|
||||||
|
with torch.no_grad():
|
||||||
|
cache.prepare(x)
|
||||||
|
expected = projection(x)
|
||||||
|
cache.project("a", projection, x, lambda: projection(x))
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def pre_hook(module, args):
|
||||||
|
if module is linear:
|
||||||
|
calls.append(module)
|
||||||
|
return (args[0] + 1,)
|
||||||
|
|
||||||
|
def post_hook(module, args, output):
|
||||||
|
if module is linear:
|
||||||
|
calls.append(module)
|
||||||
|
return output + 1
|
||||||
|
|
||||||
|
if kind == "pre":
|
||||||
|
expected = linear(x + 1)
|
||||||
|
register = (
|
||||||
|
torch.nn.modules.module.register_module_forward_pre_hook
|
||||||
|
if scope == "global"
|
||||||
|
else linear.register_forward_pre_hook
|
||||||
|
)
|
||||||
|
hook = register(pre_hook)
|
||||||
|
else:
|
||||||
|
expected = expected + 1
|
||||||
|
register = (
|
||||||
|
torch.nn.modules.module.register_module_forward_hook
|
||||||
|
if scope == "global"
|
||||||
|
else linear.register_forward_hook
|
||||||
|
)
|
||||||
|
hook = register(post_hook)
|
||||||
|
try:
|
||||||
|
for _ in range(2):
|
||||||
|
assert torch.equal(cache.project("a", projection, x, lambda: projection(x)), expected)
|
||||||
|
assert len(calls) == 2
|
||||||
|
finally:
|
||||||
|
hook.remove()
|
||||||
|
assert torch.equal(cache.project("a", projection, x, lambda: projection(x)), projection(x))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("autocast_at_prepare", [False, True])
|
||||||
|
def test_numerical_settings_follow_projection_context(mocker, autocast_at_prepare):
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(4, 4)
|
||||||
|
x = torch.randn(2, 4)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
with torch.no_grad():
|
||||||
|
with torch.autocast("cpu", dtype=torch.bfloat16, enabled=autocast_at_prepare):
|
||||||
|
cache.prepare(x)
|
||||||
|
for enabled in (False, True, False, True):
|
||||||
|
with torch.autocast("cpu", dtype=torch.bfloat16, enabled=enabled):
|
||||||
|
actual = cache.project("a", linear, x, compute)
|
||||||
|
expected = linear(x)
|
||||||
|
assert actual.dtype == expected.dtype
|
||||||
|
assert torch.equal(actual, expected)
|
||||||
|
assert compute.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"setting",
|
||||||
|
["allow_tf32", "allow_bf16_reduced_precision_reduction", "allow_fp16_reduced_precision_reduction"],
|
||||||
|
)
|
||||||
|
def test_matmul_settings_invalidate_prepared_projection(monkeypatch, mocker, setting):
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
x = torch.ones(1, 2)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
with torch.no_grad():
|
||||||
|
cache.prepare(x)
|
||||||
|
cache.project("a", linear, x, compute)
|
||||||
|
monkeypatch.setattr(torch.backends.cuda.matmul, setting, not getattr(torch.backends.cuda.matmul, setting))
|
||||||
|
assert torch.equal(cache.project("a", linear, x, compute), linear(x))
|
||||||
|
assert compute.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_and_grad_paths_keep_original_computation(monkeypatch, mocker):
|
||||||
|
import vllm_omni.diffusion.cache.exact_projection_cache as module
|
||||||
|
|
||||||
|
monkeypatch.setattr(module, "tensor_digest", mocker.Mock(side_effect=AssertionError("must not hash")))
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
x = torch.ones(1, 2, requires_grad=True)
|
||||||
|
for cache in (ExactProjectionCache(max_bytes=0), ExactProjectionCache()):
|
||||||
|
cache.prepare(x)
|
||||||
|
cache.project("a", linear, x, lambda: linear(x)).sum().backward()
|
||||||
|
assert x.grad is not None
|
||||||
|
with torch.no_grad():
|
||||||
|
cache = ExactProjectionCache(max_bytes=0)
|
||||||
|
cache.prepare(x)
|
||||||
|
assert torch.equal(cache.project("a", linear, x, lambda: linear(x)), linear(x))
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp_rank_miss_forces_every_rank_to_recompute(monkeypatch, mocker):
|
||||||
|
import vllm.distributed
|
||||||
|
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
x = torch.ones(1, 2)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
with torch.no_grad():
|
||||||
|
cache.prepare(x)
|
||||||
|
cache.project("a", linear, x, compute)
|
||||||
|
votes = mocker.Mock(side_effect=[torch.tensor([1]), torch.tensor([2])])
|
||||||
|
monkeypatch.setattr(vllm.distributed, "get_tensor_model_parallel_world_size", lambda: 2)
|
||||||
|
monkeypatch.setattr(vllm.distributed, "get_tp_group", lambda: SimpleNamespace(all_reduce=votes))
|
||||||
|
cache.project("a", linear, x, compute)
|
||||||
|
assert compute.call_count == 2
|
||||||
|
cache.project("a", linear, x, compute)
|
||||||
|
assert compute.call_count == 2
|
||||||
|
assert votes.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_compiled_execution_bypasses_runtime_and_precomputed_results(monkeypatch, mocker):
|
||||||
|
import vllm_omni.diffusion.cache.exact_projection_cache as module
|
||||||
|
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
x = torch.ones(1, 2)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
with torch.no_grad():
|
||||||
|
cache.prepare(x)
|
||||||
|
cache.project("modulation", linear, x, compute)
|
||||||
|
monkeypatch.setattr(torch.compiler, "is_compiling", lambda: True)
|
||||||
|
monkeypatch.setattr(module, "tensor_digest", mocker.Mock(side_effect=AssertionError("must not hash")))
|
||||||
|
seeded = mocker.patch.object(cache, "_lookup_precomputed", side_effect=AssertionError("must not seed"))
|
||||||
|
# An input prepared before compilation must also bypass the warm entry.
|
||||||
|
assert torch.equal(cache.project("modulation", linear, x, compute), linear(x))
|
||||||
|
cache.prepare(x)
|
||||||
|
assert torch.equal(cache.project("modulation", linear, x, compute), linear(x))
|
||||||
|
assert compute.call_count == 3
|
||||||
|
seeded.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("world_size", [1, 2])
|
||||||
|
def test_precomputed_extension_is_used_only_at_tp1(monkeypatch, mocker, world_size):
|
||||||
|
import vllm.distributed
|
||||||
|
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
x = torch.ones(1, 2)
|
||||||
|
monkeypatch.setattr(vllm.distributed, "get_tensor_model_parallel_world_size", lambda: world_size)
|
||||||
|
votes = mocker.Mock(side_effect=[torch.tensor([0]), torch.tensor([world_size])])
|
||||||
|
monkeypatch.setattr(vllm.distributed, "get_tp_group", lambda: SimpleNamespace(all_reduce=votes))
|
||||||
|
with torch.no_grad():
|
||||||
|
expected = linear(x)
|
||||||
|
seeded = mocker.patch.object(cache, "_lookup_precomputed", return_value=expected)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
cache.prepare(x)
|
||||||
|
for _ in range(2):
|
||||||
|
assert torch.equal(cache.project("conditioning.scale_shift", linear, x, compute), expected)
|
||||||
|
assert seeded.call_count == (1 if world_size == 1 else 0)
|
||||||
|
assert compute.call_count == (0 if world_size == 1 else 1)
|
||||||
|
assert cache.hits == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_inference_weights_without_versions_use_safe_runtime_path(mocker):
|
||||||
|
with torch.inference_mode():
|
||||||
|
linear = torch.nn.Linear(2, 2)
|
||||||
|
x = torch.ones(1, 2)
|
||||||
|
cache = ExactProjectionCache()
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(x))
|
||||||
|
cache.prepare(x)
|
||||||
|
cache.project("a", linear, x, compute)
|
||||||
|
linear.weight.add_(1)
|
||||||
|
assert torch.equal(cache.project("a", linear, x, compute), linear(x))
|
||||||
|
assert compute.call_count == 2 and not cache._entries
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
from dataclasses import asdict
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
from safetensors.torch import save_file
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3 import minimax_h3_transformer as h3
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3.adaln_cache import (
|
||||||
|
FORMAT_VERSION,
|
||||||
|
MODES,
|
||||||
|
MiniMaxH3AdalnCache,
|
||||||
|
MiniMaxH3RuntimeAdalnCache,
|
||||||
|
build_cache,
|
||||||
|
canonical_json,
|
||||||
|
input_names,
|
||||||
|
schedule_contract,
|
||||||
|
tensor_digest,
|
||||||
|
timestep_plans,
|
||||||
|
)
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3.pipeline_minimax_h3 import MiniMaxH3Pipeline
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def tp1(monkeypatch):
|
||||||
|
import vllm.distributed
|
||||||
|
|
||||||
|
monkeypatch.setattr(vllm.distributed, "get_tensor_model_parallel_world_size", lambda: 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _contract(mode="t2va", **updates):
|
||||||
|
values = dict(
|
||||||
|
mode=mode, num_steps=4, base_schedule=(0.999, 0.749, 0.5, 0.25, 0.0), flow_shift=12.0, audio_flow_shift=3.0
|
||||||
|
)
|
||||||
|
return schedule_contract(**(values | updates))
|
||||||
|
|
||||||
|
|
||||||
|
def _fixture(tmp_path, mode="t2va", adapter=None, *, base_model=False):
|
||||||
|
arch = h3.MiniMaxH3DiTArchConfig(
|
||||||
|
num_layers=2, hidden_size=8, timestep_input_dim=4, time_embed_hidden_size=8, time_embed_dim=4
|
||||||
|
)
|
||||||
|
generator = torch.Generator().manual_seed(4)
|
||||||
|
weights = {}
|
||||||
|
for name, shape in {
|
||||||
|
"time_embedder.proj_in.weight": (8, 4),
|
||||||
|
"time_embedder.proj_in.bias": (8,),
|
||||||
|
"time_embedder.proj_out.weight": (4, 8),
|
||||||
|
"time_embedder.proj_out.bias": (4,),
|
||||||
|
}.items():
|
||||||
|
weights[name] = torch.randn(shape, generator=generator)
|
||||||
|
for name in sorted(input_names(arch.num_layers)):
|
||||||
|
if name.startswith("time_embedder."):
|
||||||
|
continue
|
||||||
|
width = (2 if name.startswith("final_layer.") else 18) * arch.hidden_size
|
||||||
|
shape = (width, arch.time_embed_dim) if name.endswith("weight") else (width,)
|
||||||
|
weights[name] = torch.randn(shape, generator=generator).to(torch.bfloat16)
|
||||||
|
variant = "ref2va" if mode.startswith("ref2va") else "fl2va"
|
||||||
|
payload, manifest = build_cache(
|
||||||
|
arch,
|
||||||
|
weights.items(),
|
||||||
|
contract=_contract(mode, **({"num_steps": 50, "base_schedule": None} if base_model else {})),
|
||||||
|
model_variant=variant,
|
||||||
|
adapter_sha256=adapter,
|
||||||
|
device=torch.device("cpu"),
|
||||||
|
)
|
||||||
|
path = tmp_path / "adaln.safetensors"
|
||||||
|
_save(path, payload, manifest)
|
||||||
|
return arch, weights, payload, manifest, path
|
||||||
|
|
||||||
|
|
||||||
|
def _save(path, payload, manifest):
|
||||||
|
save_file(payload, str(path), metadata={"format_version": FORMAT_VERSION, "manifest": canonical_json(manifest)})
|
||||||
|
|
||||||
|
|
||||||
|
def _ready(arch, weights, path, variant="fl2va", adapter=None):
|
||||||
|
cache = MiniMaxH3AdalnCache(arch, path=str(path), model_variant=variant)
|
||||||
|
cache.bind_adapter(adapter)
|
||||||
|
for name, weight in weights.items():
|
||||||
|
cache.verify_weight(name, weight)
|
||||||
|
cache.finish_loading(torch.device("cpu"))
|
||||||
|
return cache
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", MODES)
|
||||||
|
@pytest.mark.parametrize("base_model", [False, True], ids=["explicit-ladder", "base-h3-50"])
|
||||||
|
def test_builder_projection_parity_all_tasks(tmp_path, mode, base_model):
|
||||||
|
arch, weights, payload, manifest, path = _fixture(tmp_path, mode, base_model=base_model)
|
||||||
|
cache = _ready(arch, weights, path, manifest["model_variant"])
|
||||||
|
for i, plan in enumerate(timestep_plans(manifest["schedule"])):
|
||||||
|
blocks, final = cache.lookup(plan)
|
||||||
|
embedding = payload["time_embeddings"][i, : len(plan)]
|
||||||
|
activated = torch.nn.functional.silu(embedding).to(torch.bfloat16)
|
||||||
|
for block_index, pieces in enumerate(blocks):
|
||||||
|
prefix = f"blocks.{block_index}.adaln_proj.linear"
|
||||||
|
expected = torch.nn.functional.linear(activated, weights[prefix + ".weight"], weights[prefix + ".bias"])
|
||||||
|
actual = torch.stack(pieces, dim=1).reshape(len(plan), -1)
|
||||||
|
assert torch.equal(actual, expected)
|
||||||
|
prefix = "final_layer.adaln_proj.linear"
|
||||||
|
expected = torch.nn.functional.linear(activated, weights[prefix + ".weight"], weights[prefix + ".bias"])
|
||||||
|
assert torch.equal(torch.stack(final, dim=1).reshape(len(plan), -1), expected)
|
||||||
|
assert not cache.state_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("change", ["variant", "architecture", "weights", "payload", "plans", "nan", "version"])
|
||||||
|
def test_reject_invalid_sidecar(tmp_path, change):
|
||||||
|
arch, weights, payload, manifest, path = _fixture(tmp_path)
|
||||||
|
payload = {key: value.clone() for key, value in payload.items()}
|
||||||
|
manifest = copy.deepcopy(manifest)
|
||||||
|
if change == "variant":
|
||||||
|
manifest["model_variant"] = "ref2va"
|
||||||
|
elif change == "architecture":
|
||||||
|
manifest["architecture"]["hidden_size"] += 1
|
||||||
|
elif change == "weights":
|
||||||
|
manifest["weights"].pop(next(iter(weights)))
|
||||||
|
elif change == "payload":
|
||||||
|
payload["block_params"][0, 0, 0, 0] += 1
|
||||||
|
elif change == "plans":
|
||||||
|
payload["plan_timesteps"][0, 0] += 0.01
|
||||||
|
manifest["payload"]["plan_timesteps"] = tensor_digest(payload["plan_timesteps"])
|
||||||
|
elif change == "nan":
|
||||||
|
payload["final_params"][0, 0, 0] = float("nan")
|
||||||
|
manifest["payload"]["final_params"] = tensor_digest(payload["final_params"])
|
||||||
|
_save(path, payload, manifest)
|
||||||
|
if change == "version":
|
||||||
|
save_file(payload, str(path), metadata={"format_version": "3"})
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
MiniMaxH3AdalnCache(arch, path=str(path), model_variant="fl2va")
|
||||||
|
|
||||||
|
|
||||||
|
def test_weight_adapter_schedule_and_uncovered_timestep_checks(tmp_path):
|
||||||
|
arch, weights, _, manifest, path = _fixture(tmp_path, adapter="a" * 64)
|
||||||
|
cache = MiniMaxH3AdalnCache(arch, path=str(path), model_variant="fl2va")
|
||||||
|
with pytest.raises(ValueError, match="adapter mismatch"):
|
||||||
|
cache.bind_adapter(None)
|
||||||
|
cache.bind_adapter("a" * 64)
|
||||||
|
with pytest.raises(ValueError, match="Missing"):
|
||||||
|
cache.finish_loading(torch.device("cpu"))
|
||||||
|
name = next(iter(weights))
|
||||||
|
with pytest.raises(ValueError, match="effective weight mismatch"):
|
||||||
|
cache.verify_weight(name, weights[name] + 1)
|
||||||
|
for name, weight in weights.items():
|
||||||
|
cache.verify_weight(name, weight)
|
||||||
|
cache.finish_loading(torch.device("cpu"))
|
||||||
|
for update in (
|
||||||
|
{"flow_shift": 10.0},
|
||||||
|
{"audio_flow_shift": 4.0},
|
||||||
|
{"mode": "fl2va"},
|
||||||
|
{"base_schedule": [0.99, 0.74, 0.5, 0.25, 0]},
|
||||||
|
):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
cache.check_request(**(manifest["schedule"] | update))
|
||||||
|
with pytest.raises(ValueError, match="does not cover"):
|
||||||
|
cache.lookup(torch.tensor([0.123]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapter_switch_invalidates_runtime_cache(mocker):
|
||||||
|
pipeline = object.__new__(MiniMaxH3Pipeline)
|
||||||
|
torch.nn.Module.__init__(pipeline)
|
||||||
|
cache = mocker.Mock()
|
||||||
|
pipeline.transformer = SimpleNamespace(adaln_cache=cache)
|
||||||
|
from vllm.lora.request import LoRARequest
|
||||||
|
|
||||||
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||||
|
|
||||||
|
sampling = OmniDiffusionSamplingParams(lora_request=LoRARequest("one", 1, "one"), lora_scale=1.0)
|
||||||
|
pipeline._prepare_adaln_adapter(sampling)
|
||||||
|
pipeline._prepare_adaln_adapter(sampling)
|
||||||
|
assert cache.clear.call_count == 1
|
||||||
|
sampling.lora_scale = 0.5
|
||||||
|
pipeline._prepare_adaln_adapter(sampling)
|
||||||
|
sampling.lora_request = None
|
||||||
|
pipeline._prepare_adaln_adapter(sampling)
|
||||||
|
assert cache.clear.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_sidecar_never_drops_runtime_weights(tmp_path):
|
||||||
|
arch, weights, _, _, path = _fixture(tmp_path)
|
||||||
|
candidate = MiniMaxH3AdalnCache(arch, path=str(path), model_variant="fl2va")
|
||||||
|
component = SimpleNamespace(_adaln_sidecar_candidate=candidate)
|
||||||
|
changed = list(weights.items())
|
||||||
|
changed[0] = changed[0][0], changed[0][1] + 1
|
||||||
|
result = list(MiniMaxH3Pipeline._verify_adaln_weights(component, iter(changed)))
|
||||||
|
assert len(result) == len(changed)
|
||||||
|
assert all(tensor is source for (_, tensor), (_, source) in zip(result, changed))
|
||||||
|
assert component._adaln_sidecar_candidate is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tp_size", [1, 2])
|
||||||
|
def test_native_constructor_enables_cache_by_default(monkeypatch, tp_size):
|
||||||
|
from tests.diffusion.models.minimax_h3.test_minimax_h3_quantization import (
|
||||||
|
_FakeAttention,
|
||||||
|
_FakeLinear,
|
||||||
|
_small_od_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in ("ColumnParallelLinear", "RowParallelLinear", "MergedColumnParallelLinear", "QKVParallelLinear"):
|
||||||
|
monkeypatch.setattr(h3, name, _FakeLinear)
|
||||||
|
monkeypatch.setattr(h3, "Attention", _FakeAttention)
|
||||||
|
monkeypatch.setattr(h3, "get_tensor_model_parallel_world_size", lambda: tp_size)
|
||||||
|
config = _small_od_config()
|
||||||
|
model = h3.MiniMaxH3DiTModel(config, diffusers_weights=False)
|
||||||
|
assert model.adaln_cache.max_bytes > 0
|
||||||
|
assert model.blocks[0].adaln_proj._adaln_cache is model.adaln_cache
|
||||||
|
assert model.final_layer.adaln_proj._adaln_cache is model.adaln_cache
|
||||||
|
assert "blocks.0.adaln_proj.linear.weight" in dict(model.named_parameters())
|
||||||
|
config.cache_config = {"minimax_h3_adaln_cache": False}
|
||||||
|
disabled = h3.MiniMaxH3DiTModel(config, diffusers_weights=False)
|
||||||
|
assert disabled.adaln_cache.max_bytes == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_sidecar_seeds_runtime_projection(tmp_path, mocker):
|
||||||
|
arch, weights, payload, _, path = _fixture(tmp_path)
|
||||||
|
sidecar = _ready(arch, weights, path)
|
||||||
|
runtime = MiniMaxH3RuntimeAdalnCache()
|
||||||
|
name = "blocks.0.adaln_proj.linear"
|
||||||
|
linear = torch.nn.Linear(arch.time_embed_dim, 18 * arch.hidden_size, dtype=torch.bfloat16)
|
||||||
|
with torch.no_grad():
|
||||||
|
linear.weight.copy_(weights[name + ".weight"])
|
||||||
|
linear.bias.copy_(weights[name + ".bias"])
|
||||||
|
runtime.seed(sidecar, {name: linear})
|
||||||
|
x = payload["time_embeddings"][1, :2].clone()
|
||||||
|
runtime.prepare(x)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(torch.nn.functional.silu(x).to(torch.bfloat16)))
|
||||||
|
result = runtime.project(name, linear, x, compute)
|
||||||
|
assert compute.call_count == 0
|
||||||
|
assert torch.equal(result, compute())
|
||||||
|
linear.weight.add_(1)
|
||||||
|
runtime.project(name, linear, x, compute)
|
||||||
|
assert compute.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_failed_seed_does_not_attach_sidecar(tmp_path):
|
||||||
|
arch, weights, _, _, path = _fixture(tmp_path)
|
||||||
|
sidecar = _ready(arch, weights, path)
|
||||||
|
runtime = MiniMaxH3RuntimeAdalnCache()
|
||||||
|
linear = torch.nn.Linear(arch.time_embed_dim, 18 * arch.hidden_size, dtype=torch.bfloat16)
|
||||||
|
with torch.inference_mode():
|
||||||
|
unversioned = torch.nn.Linear(arch.time_embed_dim, 2 * arch.hidden_size, dtype=torch.bfloat16)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Inference tensors do not track version counter"):
|
||||||
|
runtime.seed(sidecar, {"blocks.0.adaln_proj.linear": linear, "final_layer.adaln_proj.linear": unversioned})
|
||||||
|
|
||||||
|
assert runtime.sidecar is None
|
||||||
|
assert runtime._sidecar_signatures == {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", MODES)
|
||||||
|
def test_sidecar_prepare_uses_one_fingerprint_without_tensor_scan(tmp_path, mocker, mode):
|
||||||
|
import vllm_omni.diffusion.cache.exact_projection_cache as cache_module
|
||||||
|
|
||||||
|
arch, weights, payload, manifest, path = _fixture(tmp_path, mode=mode, base_model=True)
|
||||||
|
sidecar = _ready(arch, weights, path, manifest["model_variant"])
|
||||||
|
runtime = MiniMaxH3RuntimeAdalnCache()
|
||||||
|
name = "final_layer.adaln_proj.linear"
|
||||||
|
linear = torch.nn.Linear(arch.time_embed_dim, 2 * arch.hidden_size, dtype=torch.bfloat16)
|
||||||
|
with torch.no_grad():
|
||||||
|
linear.weight.copy_(weights[name + ".weight"])
|
||||||
|
linear.bias.copy_(weights[name + ".bias"])
|
||||||
|
runtime.seed(sidecar, {name: linear})
|
||||||
|
count = int(payload["plan_lengths"][-1])
|
||||||
|
embedding = payload["time_embeddings"][-1, :count].clone()
|
||||||
|
expected = linear(torch.nn.functional.silu(embedding).to(torch.bfloat16))
|
||||||
|
comparisons = mocker.spy(torch, "equal")
|
||||||
|
fingerprint = mocker.spy(cache_module, "tensor_digest")
|
||||||
|
compute = mocker.Mock(side_effect=AssertionError("matching sidecar must seed the projection"))
|
||||||
|
|
||||||
|
runtime.prepare(embedding)
|
||||||
|
actual = runtime.project(name, linear, embedding, compute)
|
||||||
|
|
||||||
|
comparisons.assert_not_called()
|
||||||
|
fingerprint.assert_called_once()
|
||||||
|
assert torch.equal(actual, expected)
|
||||||
|
compute.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sidecar_rechecks_numerical_environment_at_projection(tmp_path, mocker):
|
||||||
|
arch, weights, payload, _, path = _fixture(tmp_path)
|
||||||
|
sidecar = _ready(arch, weights, path)
|
||||||
|
runtime = MiniMaxH3RuntimeAdalnCache()
|
||||||
|
name = "final_layer.adaln_proj.linear"
|
||||||
|
linear = torch.nn.Linear(arch.time_embed_dim, 2 * arch.hidden_size, dtype=torch.bfloat16)
|
||||||
|
with torch.no_grad():
|
||||||
|
linear.weight.copy_(weights[name + ".weight"])
|
||||||
|
linear.bias.copy_(weights[name + ".bias"])
|
||||||
|
runtime.seed(sidecar, {name: linear})
|
||||||
|
count = int(payload["plan_lengths"][-1])
|
||||||
|
embedding = payload["time_embeddings"][-1, :count].clone()
|
||||||
|
runtime.prepare(embedding)
|
||||||
|
compute = mocker.Mock(side_effect=lambda: linear(torch.nn.functional.silu(embedding).to(torch.bfloat16)))
|
||||||
|
precision = torch.get_float32_matmul_precision()
|
||||||
|
try:
|
||||||
|
torch.set_float32_matmul_precision("high" if precision == "highest" else "highest")
|
||||||
|
actual = runtime.project(name, linear, embedding, compute)
|
||||||
|
compute.assert_called_once()
|
||||||
|
assert torch.equal(actual, compute())
|
||||||
|
finally:
|
||||||
|
torch.set_float32_matmul_precision(precision)
|
||||||
|
|
||||||
|
|
||||||
|
def test_builder_rejects_missing_or_duplicate_weights(tmp_path):
|
||||||
|
arch, weights, _, manifest, _ = _fixture(tmp_path)
|
||||||
|
for source in (list(weights.items())[:-1], [*weights.items(), next(iter(weights.items()))]):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
build_cache(
|
||||||
|
arch,
|
||||||
|
source,
|
||||||
|
contract=manifest["schedule"],
|
||||||
|
model_variant="fl2va",
|
||||||
|
adapter_sha256=None,
|
||||||
|
device=torch.device("cpu"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_checkpoint_builder_cli(tmp_path, monkeypatch):
|
||||||
|
from tools.minimax_h3.build_adaln_cache import main
|
||||||
|
|
||||||
|
arch, weights, _, _, _ = _fixture(tmp_path)
|
||||||
|
root = tmp_path / "transformer"
|
||||||
|
root.mkdir()
|
||||||
|
(root / "config.json").write_text(canonical_json(asdict(arch)))
|
||||||
|
save_file(weights, str(root / "model.safetensors"))
|
||||||
|
output = tmp_path / "cli.safetensors"
|
||||||
|
argv = [
|
||||||
|
"build_adaln_cache",
|
||||||
|
"--transformer-path",
|
||||||
|
str(root),
|
||||||
|
"--output",
|
||||||
|
str(output),
|
||||||
|
"--model-variant",
|
||||||
|
"fl2va",
|
||||||
|
"--num-inference-steps",
|
||||||
|
"4",
|
||||||
|
"--device",
|
||||||
|
"cpu",
|
||||||
|
]
|
||||||
|
monkeypatch.setattr("sys.argv", argv)
|
||||||
|
main()
|
||||||
|
_ready(arch, weights, output)
|
||||||
|
with pytest.raises(ValueError, match="overwrite"):
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("base_model", [False, True], ids=["fasth3-four", "base-h3-50"])
|
||||||
|
def test_runtime_projection_matches_sidecar_using_actual_h3_forwards(tmp_path, monkeypatch, mocker, base_model):
|
||||||
|
class Linear(torch.nn.Linear):
|
||||||
|
def __init__(self, in_features, out_features, *, params_dtype, bias=True, **kwargs):
|
||||||
|
super().__init__(in_features, out_features, bias=bias, dtype=params_dtype)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
return super().forward(x), None
|
||||||
|
|
||||||
|
monkeypatch.setattr(h3, "ColumnParallelLinear", Linear)
|
||||||
|
monkeypatch.setattr(h3, "RowParallelLinear", Linear)
|
||||||
|
arch, weights, payload, manifest, _ = _fixture(tmp_path, base_model=base_model)
|
||||||
|
embedder = h3.MiniMaxH3TimeEmbedder(arch, prefix="time_embedder")
|
||||||
|
runtime = MiniMaxH3RuntimeAdalnCache()
|
||||||
|
proj = h3.MiniMaxH3AdalnProj(
|
||||||
|
arch,
|
||||||
|
18 * arch.hidden_size,
|
||||||
|
None,
|
||||||
|
expand_ratio=6,
|
||||||
|
modality_num=3,
|
||||||
|
prefix="blocks.0.adaln_proj",
|
||||||
|
adaln_cache=runtime,
|
||||||
|
)
|
||||||
|
embedder.load_state_dict(
|
||||||
|
{
|
||||||
|
name.removeprefix("time_embedder."): value
|
||||||
|
for name, value in weights.items()
|
||||||
|
if name.startswith("time_embedder.")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
proj.load_state_dict(
|
||||||
|
{
|
||||||
|
name.removeprefix("blocks.0.adaln_proj."): value
|
||||||
|
for name, value in weights.items()
|
||||||
|
if name.startswith("blocks.0.adaln_proj.")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
calls = mocker.spy(proj.linear, "forward")
|
||||||
|
with torch.no_grad():
|
||||||
|
for _ in range(2):
|
||||||
|
for i, plan in enumerate(timestep_plans(manifest["schedule"])):
|
||||||
|
embedding = embedder(plan)
|
||||||
|
assert torch.equal(embedding, payload["time_embeddings"][i, : len(plan)])
|
||||||
|
runtime.prepare(embedding)
|
||||||
|
actual = torch.stack(proj(embedding), dim=1).reshape(len(plan), -1)
|
||||||
|
assert torch.equal(actual, payload["block_params"][i, : len(plan), 0])
|
||||||
|
plan_count = len(timestep_plans(manifest["schedule"]))
|
||||||
|
assert calls.call_count == plan_count and runtime.hits == plan_count
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("enforce_eager", [False, True], ids=["compiled", "eager"])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"key,mode",
|
||||||
|
[("minimax_h3_adaln_cache_path", "t2va"), ("minimax_h3_ref_adaln_cache_path", "ref2va-mixed")],
|
||||||
|
)
|
||||||
|
def test_optional_sidecar_requires_eager_before_reading_payload(tmp_path, mocker, enforce_eager, key, mode):
|
||||||
|
import vllm_omni.diffusion.models.minimax_h3.adaln_cache as cache_module
|
||||||
|
|
||||||
|
arch, _, _, manifest, path = _fixture(tmp_path, mode=mode)
|
||||||
|
open_sidecar = mocker.spy(cache_module, "safe_open")
|
||||||
|
pipeline = object.__new__(MiniMaxH3Pipeline)
|
||||||
|
torch.nn.Module.__init__(pipeline)
|
||||||
|
pipeline.od_config = SimpleNamespace(cache_config={key: str(path)}, enforce_eager=enforce_eager)
|
||||||
|
transformer = SimpleNamespace(arch=arch, adaln_cache=MiniMaxH3RuntimeAdalnCache())
|
||||||
|
|
||||||
|
pipeline._configure_adaln_sidecar(transformer, key, manifest["model_variant"], None, eligible=True)
|
||||||
|
|
||||||
|
if enforce_eager:
|
||||||
|
open_sidecar.assert_called_once()
|
||||||
|
assert isinstance(transformer._adaln_sidecar_candidate, MiniMaxH3AdalnCache)
|
||||||
|
assert transformer._adaln_sidecar_candidate.path == str(path)
|
||||||
|
else:
|
||||||
|
open_sidecar.assert_not_called()
|
||||||
|
assert not hasattr(transformer, "_adaln_sidecar_candidate")
|
||||||
|
# The normal load completion must not install or move a skipped payload.
|
||||||
|
pipeline._finish_adaln_sidecar(transformer)
|
||||||
|
assert transformer.adaln_cache.sidecar is None
|
||||||
|
assert transformer.adaln_cache.max_bytes > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_corrupt_optional_sidecar_falls_back_without_rejecting_model(tmp_path):
|
||||||
|
path = tmp_path / "truncated.safetensors"
|
||||||
|
path.write_bytes(b"truncated")
|
||||||
|
pipeline = object.__new__(MiniMaxH3Pipeline)
|
||||||
|
torch.nn.Module.__init__(pipeline)
|
||||||
|
pipeline.od_config = SimpleNamespace(cache_config={"cache": str(path)}, enforce_eager=True)
|
||||||
|
transformer = SimpleNamespace(arch=h3.MiniMaxH3DiTArchConfig(), adaln_cache=MiniMaxH3RuntimeAdalnCache())
|
||||||
|
pipeline._configure_adaln_sidecar(transformer, "cache", "fl2va", None, eligible=True)
|
||||||
|
assert not hasattr(transformer, "_adaln_sidecar_candidate")
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
"""Real vLLM projection/collective parity without loading an H3 checkpoint."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
import torch.multiprocessing as mp
|
||||||
|
|
||||||
|
from tests.helpers.mark import hardware_marks
|
||||||
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cuda]
|
||||||
|
|
||||||
|
|
||||||
|
def _projection_worker(rank, world_size, rendezvous):
|
||||||
|
from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config
|
||||||
|
from vllm.distributed.parallel_state import (
|
||||||
|
cleanup_dist_env_and_memory,
|
||||||
|
init_distributed_environment,
|
||||||
|
initialize_model_parallel,
|
||||||
|
)
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3.adaln_cache import MiniMaxH3RuntimeAdalnCache
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
|
||||||
|
MiniMaxH3AdalnProj,
|
||||||
|
MiniMaxH3DiTArchConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
torch.set_num_threads(1)
|
||||||
|
device = torch.device("cuda", rank)
|
||||||
|
current_omni_platform.set_device(device)
|
||||||
|
config = VllmConfig(device_config=DeviceConfig(device="cuda"))
|
||||||
|
with set_current_vllm_config(config):
|
||||||
|
init_distributed_environment(
|
||||||
|
world_size=world_size,
|
||||||
|
rank=rank,
|
||||||
|
local_rank=rank,
|
||||||
|
distributed_init_method=f"file://{rendezvous}",
|
||||||
|
backend="nccl",
|
||||||
|
timeout=timedelta(seconds=45),
|
||||||
|
)
|
||||||
|
initialize_model_parallel(tensor_model_parallel_size=world_size)
|
||||||
|
try:
|
||||||
|
arch = MiniMaxH3DiTArchConfig(hidden_size=256, time_embed_dim=128)
|
||||||
|
cache = MiniMaxH3RuntimeAdalnCache()
|
||||||
|
generator = torch.Generator(device=device).manual_seed(11)
|
||||||
|
for name, expansion, modalities in (("blocks.0", 6, 3), ("final_layer", 2, 1)):
|
||||||
|
cache.clear()
|
||||||
|
projection = MiniMaxH3AdalnProj(
|
||||||
|
arch,
|
||||||
|
arch.hidden_size * expansion * modalities,
|
||||||
|
None,
|
||||||
|
expand_ratio=expansion,
|
||||||
|
modality_num=modalities,
|
||||||
|
prefix=name + ".adaln_proj",
|
||||||
|
adaln_cache=cache,
|
||||||
|
).to(device)
|
||||||
|
with torch.no_grad():
|
||||||
|
for parameter in projection.parameters():
|
||||||
|
parameter.normal_(std=0.05, generator=generator)
|
||||||
|
with torch.inference_mode():
|
||||||
|
for width in (1, 2, 3, 4):
|
||||||
|
embedding = torch.randn(width, arch.time_embed_dim, device=device, generator=generator)
|
||||||
|
# No prepared input: run the original vLLM projector.
|
||||||
|
cache.clear()
|
||||||
|
expected = projection(embedding)
|
||||||
|
cache.prepare(embedding)
|
||||||
|
first = projection(embedding)
|
||||||
|
hits = cache.hits
|
||||||
|
second = projection(embedding)
|
||||||
|
assert cache.hits == hits + 1
|
||||||
|
for reference, cold, warm in zip(expected, first, second, strict=True):
|
||||||
|
assert torch.equal(reference, cold)
|
||||||
|
assert torch.equal(reference, warm)
|
||||||
|
|
||||||
|
# Only one rank loses its entry. All ranks must execute
|
||||||
|
# the gather again; a local-only hit would deadlock.
|
||||||
|
if rank == world_size - 1:
|
||||||
|
cache.clear()
|
||||||
|
cache.prepare(embedding)
|
||||||
|
misses = cache.misses
|
||||||
|
rebuilt = projection(embedding)
|
||||||
|
assert cache.misses == misses + 1
|
||||||
|
for reference, actual in zip(expected, rebuilt, strict=True):
|
||||||
|
assert torch.equal(reference, actual)
|
||||||
|
|
||||||
|
if rank == world_size - 1:
|
||||||
|
projection.linear.weight.add_(0.01)
|
||||||
|
misses = cache.misses
|
||||||
|
changed = projection(embedding)
|
||||||
|
assert cache.misses == misses + 1
|
||||||
|
cache.clear()
|
||||||
|
reference = projection(embedding)
|
||||||
|
for actual, original in zip(changed, reference, strict=True):
|
||||||
|
assert torch.equal(actual, original)
|
||||||
|
finally:
|
||||||
|
cleanup_dist_env_and_memory()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"world_size",
|
||||||
|
[
|
||||||
|
pytest.param(1, marks=hardware_marks(res={"cuda": ["H100", "B200"]}, num_cards=1)),
|
||||||
|
pytest.param(2, marks=hardware_marks(res={"cuda": ["H100", "B200"]}, num_cards=2)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_real_projection_cache_parity_and_rank_local_invalidation(tmp_path, world_size):
|
||||||
|
if not current_omni_platform.is_cuda():
|
||||||
|
pytest.skip("Requires CUDA")
|
||||||
|
context = mp.spawn(
|
||||||
|
_projection_worker,
|
||||||
|
args=(world_size, str(tmp_path / "rendezvous")),
|
||||||
|
nprocs=world_size,
|
||||||
|
join=False,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if not context.join(timeout=90):
|
||||||
|
# ProcessContext may reap one successful worker per join.
|
||||||
|
if not context.join(timeout=30):
|
||||||
|
pytest.fail("AdaLN projection workers timed out")
|
||||||
|
finally:
|
||||||
|
for process in context.processes:
|
||||||
|
if process.is_alive():
|
||||||
|
process.terminate()
|
||||||
|
process.join(timeout=5)
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
"""Build an exact AdaLN sidecar from local native weights and a fixed adapter."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from safetensors import safe_open
|
||||||
|
from safetensors.torch import save_file
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3.adaln_cache import (
|
||||||
|
FORMAT_VERSION,
|
||||||
|
MODES,
|
||||||
|
MiniMaxH3AdalnCache,
|
||||||
|
build_cache,
|
||||||
|
canonical_json,
|
||||||
|
file_digest,
|
||||||
|
input_names,
|
||||||
|
schedule_contract,
|
||||||
|
)
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3.fasth3 import FastH3WeightFusion
|
||||||
|
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import MiniMaxH3DiTArchConfig
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--transformer-path", type=Path, required=True)
|
||||||
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
|
parser.add_argument("--model-variant", choices=("fl2va", "ref2va"), required=True)
|
||||||
|
parser.add_argument("--mode", choices=tuple(MODES), default="t2va")
|
||||||
|
parser.add_argument("--num-inference-steps", type=int, default=None)
|
||||||
|
parser.add_argument("--flow-shift", type=float, default=12.0)
|
||||||
|
parser.add_argument("--audio-flow-shift", type=float, default=3.0)
|
||||||
|
parser.add_argument("--base-schedule", type=float, nargs="+")
|
||||||
|
parser.add_argument("--fasth3-adapter", type=Path)
|
||||||
|
parser.add_argument("--device", default="cuda", help="Use the serving device type; cpu is for CPU-only fixtures")
|
||||||
|
args = parser.parse_args()
|
||||||
|
root = args.transformer_path.expanduser().resolve()
|
||||||
|
if args.output.exists():
|
||||||
|
raise ValueError(f"Refusing to overwrite {args.output}")
|
||||||
|
config = json.loads((root / "config.json").read_text())
|
||||||
|
arch = MiniMaxH3DiTArchConfig.from_mapping(config)
|
||||||
|
variant = "ref2va" if args.mode.startswith("ref2va") else "fl2va"
|
||||||
|
if args.model_variant != variant:
|
||||||
|
raise ValueError("Cache mode does not match --model-variant")
|
||||||
|
device = torch.device(args.device)
|
||||||
|
if device.type not in {"cpu", "cuda"}:
|
||||||
|
raise ValueError("The builder supports CPU fixtures and CUDA serving")
|
||||||
|
fusion = None
|
||||||
|
if args.fasth3_adapter is not None:
|
||||||
|
if args.mode != "t2va" or args.model_variant != "fl2va":
|
||||||
|
raise ValueError("FastH3 Preview adapters support T2VA only")
|
||||||
|
fusion = FastH3WeightFusion.from_path(
|
||||||
|
args.fasth3_adapter,
|
||||||
|
head_dim=arch.attention_head_dim,
|
||||||
|
num_blocks=arch.num_layers,
|
||||||
|
num_refiner_blocks=arch.token_refiner_num_layers,
|
||||||
|
)
|
||||||
|
if fusion is None:
|
||||||
|
raise ValueError("Not a supported fixed FastH3 adapter")
|
||||||
|
if args.base_schedule is not None and tuple(args.base_schedule) != fusion.base_schedule:
|
||||||
|
raise ValueError("Explicit schedule does not match the FastH3 adapter")
|
||||||
|
args.base_schedule = fusion.base_schedule
|
||||||
|
num_steps = args.num_inference_steps
|
||||||
|
if num_steps is None:
|
||||||
|
num_steps = len(args.base_schedule) - 1 if args.base_schedule is not None else 50
|
||||||
|
contract = schedule_contract(
|
||||||
|
mode=args.mode,
|
||||||
|
num_steps=num_steps,
|
||||||
|
base_schedule=args.base_schedule,
|
||||||
|
flow_shift=args.flow_shift,
|
||||||
|
audio_flow_shift=args.audio_flow_shift,
|
||||||
|
)
|
||||||
|
adapter_sha = file_digest(fusion.source) if fusion is not None else None
|
||||||
|
index = root / "model.safetensors.index.json"
|
||||||
|
if index.is_file():
|
||||||
|
weight_map = json.loads(index.read_text())["weight_map"]
|
||||||
|
else:
|
||||||
|
single = root / "model.safetensors"
|
||||||
|
with safe_open(str(single), framework="pt", device="cpu") as source:
|
||||||
|
weight_map = {name: single.name for name in source.keys()}
|
||||||
|
required = input_names(arch.num_layers)
|
||||||
|
if missing := required - set(weight_map):
|
||||||
|
raise ValueError(f"Native checkpoint is missing AdaLN inputs: {sorted(missing)}")
|
||||||
|
|
||||||
|
def weights():
|
||||||
|
ordered = sorted(required, key=lambda name: (not name.startswith("time_embedder."), name))
|
||||||
|
for name in ordered:
|
||||||
|
filename = (root / weight_map[name]).resolve()
|
||||||
|
if not filename.is_relative_to(root):
|
||||||
|
# HF snapshot shards may be symlinks to blobs; use the lexical
|
||||||
|
# index path for containment, while allowing those symlinks.
|
||||||
|
relative = Path(weight_map[name])
|
||||||
|
if relative.is_absolute() or ".." in relative.parts:
|
||||||
|
raise ValueError("Checkpoint index points outside the transformer directory")
|
||||||
|
with safe_open(str(filename), framework="pt", device="cpu") as source:
|
||||||
|
weight = source.get_tensor(name)
|
||||||
|
if fusion is not None:
|
||||||
|
weight = fusion.fuse(name, weight.to(device))
|
||||||
|
yield name, weight
|
||||||
|
|
||||||
|
payload, manifest = build_cache(
|
||||||
|
arch,
|
||||||
|
weights(),
|
||||||
|
contract=contract,
|
||||||
|
model_variant=args.model_variant,
|
||||||
|
adapter_sha256=adapter_sha,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
if fusion is not None and file_digest(fusion.source) != adapter_sha:
|
||||||
|
raise ValueError("Adapter changed during cache construction")
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
descriptor, temporary = tempfile.mkstemp(prefix=".adaln-", suffix=".safetensors", dir=args.output.parent)
|
||||||
|
os.close(descriptor)
|
||||||
|
try:
|
||||||
|
save_file(payload, temporary, metadata={"format_version": FORMAT_VERSION, "manifest": canonical_json(manifest)})
|
||||||
|
MiniMaxH3AdalnCache(arch, path=temporary, model_variant=args.model_variant)
|
||||||
|
# Atomic no-clobber publication on the same filesystem.
|
||||||
|
os.link(temporary, args.output)
|
||||||
|
finally:
|
||||||
|
Path(temporary).unlink(missing_ok=True)
|
||||||
|
print(f"Wrote weight-bound AdaLN sidecar: {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+3
-1
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""
|
"""
|
||||||
Cache module for diffusion model inference acceleration.
|
Cache module for diffusion model inference acceleration.
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ Cache backends are instantiated directly via their constructors and configured v
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from vllm_omni.diffusion.cache.base import CacheBackend
|
from vllm_omni.diffusion.cache.base import CacheBackend
|
||||||
|
from vllm_omni.diffusion.cache.exact_projection_cache import ExactProjectionCache
|
||||||
from vllm_omni.diffusion.cache.prompt_embed_cache import (
|
from vllm_omni.diffusion.cache.prompt_embed_cache import (
|
||||||
PromptEmbedCache,
|
PromptEmbedCache,
|
||||||
install_prompt_embed_cache,
|
install_prompt_embed_cache,
|
||||||
@@ -27,6 +28,7 @@ from vllm_omni.diffusion.cache.teacache.backend import TeaCacheBackend
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CacheBackend",
|
"CacheBackend",
|
||||||
|
"ExactProjectionCache",
|
||||||
"CacheContext",
|
"CacheContext",
|
||||||
"TeaCacheBackend",
|
"TeaCacheBackend",
|
||||||
"TeaCacheConfig",
|
"TeaCacheConfig",
|
||||||
|
|||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
"""Model-independent exact caching of small conditioning projections."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import weakref
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch import nn
|
||||||
|
from torch.nn.modules import module as module_hooks
|
||||||
|
|
||||||
|
|
||||||
|
class ExactProjectionCache:
|
||||||
|
"""Bounded, exact memoization for deterministic conditioning projections.
|
||||||
|
|
||||||
|
Models call :meth:`prepare` once for an immutable conditioning tensor, then
|
||||||
|
:meth:`project` for each leaf projection using a stable, model-defined name.
|
||||||
|
The supplied computation must depend only on that tensor, the projection's
|
||||||
|
parameters/buffers and fixed preprocessing. Include all request conditioning
|
||||||
|
in the tensor; timestep alone is insufficient for prompt/guidance-dependent
|
||||||
|
projections. Outputs returned on cache hits must be treated as read-only.
|
||||||
|
|
||||||
|
The original projection computes misses, preserving its quantization, LoRA
|
||||||
|
and TP behavior. Parameter/buffer versions guard reuse, and TP ranks vote
|
||||||
|
before skipping collectives. Call :meth:`clear` for adapter/configuration
|
||||||
|
changes or model moves. Gradient-enabled and compiled execution bypass reuse.
|
||||||
|
|
||||||
|
Each cache belongs to one model instance executing forwards serially. It
|
||||||
|
retains a bounded subset across requests, so it is independent of the
|
||||||
|
request-scoped approximate cache backends. It does not offload weights or
|
||||||
|
suppress block prefetch. Models may supply validated precomputed results by
|
||||||
|
overriding :meth:`_lookup_precomputed`; artifact loading stays model-owned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, max_bytes: int = 256 * 1024**2) -> None:
|
||||||
|
if type(max_bytes) is not int or max_bytes < 0:
|
||||||
|
raise ValueError("Projection cache budget must be a nonnegative byte count")
|
||||||
|
self.max_bytes = max_bytes
|
||||||
|
self._entries: dict[tuple[str, str, tuple[Any, ...]], tuple[Any, torch.Tensor, int]] = {}
|
||||||
|
self._bytes = 0
|
||||||
|
self._input: weakref.ReferenceType[torch.Tensor] | None = None
|
||||||
|
self._key: str | None = None
|
||||||
|
self.hits = self.misses = 0
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self._entries.clear()
|
||||||
|
self._bytes = 0
|
||||||
|
self._input = None
|
||||||
|
self._key = None
|
||||||
|
|
||||||
|
def prepare(self, embedding: torch.Tensor) -> None:
|
||||||
|
self._input = None
|
||||||
|
self._key = None
|
||||||
|
if not self.max_bytes or torch.is_grad_enabled() or torch.compiler.is_compiling():
|
||||||
|
return
|
||||||
|
self._key = tensor_digest(embedding)
|
||||||
|
self._input = weakref.ref(embedding)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _numerical_settings(device_type: str) -> tuple[Any, ...]:
|
||||||
|
# A projection may enter its own autocast context after prepare().
|
||||||
|
return (
|
||||||
|
torch.get_float32_matmul_precision(),
|
||||||
|
torch.is_autocast_enabled(device_type),
|
||||||
|
torch.get_autocast_dtype(device_type),
|
||||||
|
torch.backends.cuda.matmul.allow_tf32,
|
||||||
|
torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction,
|
||||||
|
torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_forward_hooks(module: nn.Module) -> bool:
|
||||||
|
# Match nn.Module's global hooks as well as hooks on wrapped linears.
|
||||||
|
return bool(
|
||||||
|
module_hooks._global_forward_pre_hooks
|
||||||
|
or module_hooks._global_forward_hooks
|
||||||
|
or any(child._forward_pre_hooks or child._forward_hooks for child in module.modules())
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _signature(module: nn.Module) -> tuple[Any, ...]:
|
||||||
|
# A replaced or restored storage conservatively causes a miss. The
|
||||||
|
# containing block's offload hooks still run before this method.
|
||||||
|
tensors = [*module.named_parameters(), *module.named_buffers()]
|
||||||
|
# vLLM LoRA stores these tensors outside registered buffers. Its
|
||||||
|
# suspend/resume mask can change without changing the base weights.
|
||||||
|
for field in ("lora_a_stacked", "lora_b_stacked"):
|
||||||
|
tensors.extend((f"{field}.{i}", value) for i, value in enumerate(getattr(module, field, ())))
|
||||||
|
return (
|
||||||
|
id(module),
|
||||||
|
id(getattr(module, "quant_method", None)),
|
||||||
|
getattr(module, "_diffusion_lora_active_slices", None),
|
||||||
|
*(
|
||||||
|
(name, id(value), value.data_ptr(), value._version, tuple(value.shape), value.dtype, value.device)
|
||||||
|
for name, value in tensors
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def project(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
module: nn.Module,
|
||||||
|
embedding: torch.Tensor,
|
||||||
|
compute: Callable[[], torch.Tensor],
|
||||||
|
) -> torch.Tensor:
|
||||||
|
if (
|
||||||
|
torch.is_grad_enabled()
|
||||||
|
or torch.compiler.is_compiling()
|
||||||
|
or self._key is None
|
||||||
|
or self._input is None
|
||||||
|
or self._input() is not embedding
|
||||||
|
):
|
||||||
|
return compute()
|
||||||
|
key = self._key, name, self._numerical_settings(embedding.device.type)
|
||||||
|
signature: tuple[Any, ...] | None
|
||||||
|
try:
|
||||||
|
signature = self._signature(module)
|
||||||
|
if self._has_forward_hooks(module):
|
||||||
|
# A linear hook may mutate weights or transform the output;
|
||||||
|
# executing it only on misses would change its semantics.
|
||||||
|
signature = None
|
||||||
|
except RuntimeError:
|
||||||
|
# Inference tensors without version counters cannot establish that
|
||||||
|
# weights stayed unchanged. Still participate in the TP vote.
|
||||||
|
signature = None
|
||||||
|
cached = self._entries.get(key)
|
||||||
|
hit = signature is not None and cached is not None and cached[0] == signature
|
||||||
|
from vllm.distributed import get_tensor_model_parallel_world_size, get_tp_group
|
||||||
|
|
||||||
|
world_size = get_tensor_model_parallel_world_size()
|
||||||
|
if world_size > 1:
|
||||||
|
vote = torch.tensor([int(hit)], device=embedding.device, dtype=torch.int32)
|
||||||
|
hit = int(get_tp_group().all_reduce(vote).item()) == world_size
|
||||||
|
if hit:
|
||||||
|
assert cached is not None
|
||||||
|
self.hits += 1
|
||||||
|
return cached[1]
|
||||||
|
self.misses += 1
|
||||||
|
value = (
|
||||||
|
self._lookup_precomputed(name, embedding, signature) if world_size == 1 and signature is not None else None
|
||||||
|
)
|
||||||
|
if value is None:
|
||||||
|
value = compute()
|
||||||
|
if cached is not None:
|
||||||
|
self._bytes -= self._entries.pop(key)[2]
|
||||||
|
size = value.numel() * value.element_size()
|
||||||
|
if signature is not None and self._bytes + size <= self.max_bytes:
|
||||||
|
# Retain a reusable subset when a long denoising schedule exceeds
|
||||||
|
# the budget. LRU would evict every entry before the next request
|
||||||
|
# reaches it, producing zero hits for a repeated cyclic schedule.
|
||||||
|
# Own the cached storage even if a provider returns workspace views.
|
||||||
|
self._entries[key] = (signature, value.detach().clone(), size)
|
||||||
|
self._bytes += size
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _lookup_precomputed(self, name: str, embedding: torch.Tensor, signature: Any) -> torch.Tensor | None:
|
||||||
|
"""Return a validated TP1 projection, or None to execute the original.
|
||||||
|
|
||||||
|
Called only on runtime-cache misses with a usable weight signature.
|
||||||
|
Implementations must also validate inputs, numerical settings, artifact
|
||||||
|
compatibility and any state not represented in the module signature.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_json(value: Any) -> str:
|
||||||
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_digest(tensor: torch.Tensor) -> str:
|
||||||
|
"""Hash dtype, shape and actual bytes, including BF16, without widening."""
|
||||||
|
value = tensor.detach().cpu().contiguous()
|
||||||
|
digest = hashlib.sha256(canonical_json([str(value.dtype), list(value.shape)]).encode())
|
||||||
|
digest.update(memoryview(value.reshape(-1).view(torch.uint8).numpy()))
|
||||||
|
return digest.hexdigest()
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
"""Default exact AdaLN memoization and optional weight-bound startup sidecars.
|
||||||
|
|
||||||
|
This is not a cross-step approximation. The main blocks' and final layer's
|
||||||
|
AdaLN outputs are reused only for identical inputs and unchanged weights. All
|
||||||
|
weights remain available for new schedules and adapters. Legacy experimental
|
||||||
|
sidecars without weight identities are rejected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from collections.abc import Iterable, Mapping, Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from safetensors import safe_open
|
||||||
|
from torch import nn
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.cache.exact_projection_cache import (
|
||||||
|
ExactProjectionCache,
|
||||||
|
tensor_digest,
|
||||||
|
)
|
||||||
|
from vllm_omni.diffusion.cache.exact_projection_cache import (
|
||||||
|
canonical_json as canonical_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .time_request import minimax_h3_time_shift_sigmas
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .minimax_h3_transformer import MiniMaxH3DiTArchConfig
|
||||||
|
|
||||||
|
FORMAT_VERSION = "4"
|
||||||
|
MODES = {
|
||||||
|
"t2va": ("video", "audio"),
|
||||||
|
"fl2va": ("video", "audio", "image"),
|
||||||
|
"ref2va-image": ("video", "audio", "image"),
|
||||||
|
"ref2va-audio": ("video", "audio", "audio_ref"),
|
||||||
|
"ref2va-mixed": ("video", "audio", "image", "audio_ref"),
|
||||||
|
}
|
||||||
|
PAYLOAD_NAMES = frozenset({"plan_timesteps", "plan_lengths", "time_embeddings", "block_params", "final_params"})
|
||||||
|
|
||||||
|
|
||||||
|
class MiniMaxH3RuntimeAdalnCache(ExactProjectionCache):
|
||||||
|
"""Exact projection caching with optional H3 schedule-bound sidecar results."""
|
||||||
|
|
||||||
|
def __init__(self, *, max_bytes: int = 256 * 1024**2) -> None:
|
||||||
|
super().__init__(max_bytes=max_bytes)
|
||||||
|
self.sidecar: MiniMaxH3AdalnCache | None = None
|
||||||
|
self._sidecar_signatures: dict[str, Any] = {}
|
||||||
|
self._sidecar_plan: int | None = None
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
super().clear()
|
||||||
|
self.sidecar = None
|
||||||
|
self._sidecar_signatures.clear()
|
||||||
|
self._sidecar_plan = None
|
||||||
|
|
||||||
|
def seed(self, sidecar: MiniMaxH3AdalnCache, modules: Mapping[str, nn.Module]) -> None:
|
||||||
|
# A rejected seed must not retain the sidecar's device payload.
|
||||||
|
signatures = {name: self._signature(module) for name, module in modules.items()}
|
||||||
|
self.sidecar = sidecar
|
||||||
|
self._sidecar_signatures = signatures
|
||||||
|
|
||||||
|
def prepare(self, embedding: torch.Tensor) -> None:
|
||||||
|
self._sidecar_plan = None
|
||||||
|
super().prepare(embedding)
|
||||||
|
if self._key is None or self.sidecar is None:
|
||||||
|
return
|
||||||
|
if math_identity(embedding.device) != self.sidecar.manifest["math"]:
|
||||||
|
self.sidecar = None
|
||||||
|
return
|
||||||
|
self._sidecar_plan = self.sidecar._embedding_plans.get(self._key)
|
||||||
|
|
||||||
|
def _lookup_precomputed(self, name: str, embedding: torch.Tensor, signature: Any) -> torch.Tensor | None:
|
||||||
|
if (
|
||||||
|
self.sidecar is None
|
||||||
|
or self._sidecar_plan is None
|
||||||
|
or self._sidecar_signatures.get(name) != signature
|
||||||
|
or torch.is_autocast_enabled(embedding.device.type)
|
||||||
|
or math_identity(embedding.device) != self.sidecar.manifest["math"]
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
index, count = self._sidecar_plan, embedding.shape[0]
|
||||||
|
if name.startswith("blocks."):
|
||||||
|
return self.sidecar.block_params[index, :count, int(name.split(".")[1])].contiguous()
|
||||||
|
return self.sidecar.final_params[index, :count].contiguous()
|
||||||
|
|
||||||
|
|
||||||
|
def file_digest(path: str | Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with Path(path).open("rb") as source:
|
||||||
|
while chunk := source.read(16 * 1024 * 1024):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def projection_names(num_layers: int) -> frozenset[str]:
|
||||||
|
prefixes = [f"blocks.{i}.adaln_proj.linear" for i in range(num_layers)]
|
||||||
|
prefixes.append("final_layer.adaln_proj.linear")
|
||||||
|
return frozenset(f"{prefix}.{kind}" for prefix in prefixes for kind in ("weight", "bias"))
|
||||||
|
|
||||||
|
|
||||||
|
def input_names(num_layers: int) -> frozenset[str]:
|
||||||
|
return projection_names(num_layers) | {
|
||||||
|
f"time_embedder.{module}.{kind}" for module in ("proj_in", "proj_out") for kind in ("weight", "bias")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def architecture(arch: MiniMaxH3DiTArchConfig) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
key: getattr(arch, key)
|
||||||
|
for key in ("num_layers", "hidden_size", "timestep_input_dim", "time_embed_hidden_size", "time_embed_dim")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def math_identity(device: torch.device) -> dict[str, Any]:
|
||||||
|
"""Conservatively bind the builder's numerical environment (TP1 only)."""
|
||||||
|
result = {
|
||||||
|
"torch": str(torch.__version__),
|
||||||
|
"device_type": device.type,
|
||||||
|
"float32_matmul_precision": torch.get_float32_matmul_precision(),
|
||||||
|
}
|
||||||
|
if device.type == "cuda":
|
||||||
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
|
capability = current_omni_platform.get_device_capability(device.index or 0)
|
||||||
|
result.update(
|
||||||
|
cuda=torch.version.cuda,
|
||||||
|
capability=list(capability) if capability is not None else None,
|
||||||
|
allow_tf32=torch.backends.cuda.matmul.allow_tf32,
|
||||||
|
allow_bf16_reduced_precision_reduction=torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_contract(
|
||||||
|
*, mode: str, num_steps: int, base_schedule: Sequence[float] | None, flow_shift: float, audio_flow_shift: float
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if mode not in MODES:
|
||||||
|
raise ValueError(f"Unsupported AdaLN cache mode: {mode}")
|
||||||
|
if type(num_steps) is not int or num_steps < (2 if base_schedule is None else 1):
|
||||||
|
raise ValueError("AdaLN cache requires a positive complete denoising schedule")
|
||||||
|
for value in (flow_shift, audio_flow_shift):
|
||||||
|
if not math.isfinite(value) or value <= 0:
|
||||||
|
raise ValueError("AdaLN cache shifts must be finite and positive")
|
||||||
|
if base_schedule is not None:
|
||||||
|
from vllm_omni.diffusion.sched.sigma_schedule import DMD2SigmaSchedule
|
||||||
|
|
||||||
|
schedule = DMD2SigmaSchedule.from_positions(base_schedule)
|
||||||
|
if schedule.num_inference_steps != num_steps:
|
||||||
|
raise ValueError("AdaLN cache step count does not match the explicit schedule")
|
||||||
|
base_schedule = schedule.base_schedule
|
||||||
|
return dict(
|
||||||
|
mode=mode,
|
||||||
|
num_steps=num_steps,
|
||||||
|
base_schedule=list(base_schedule) if base_schedule is not None else None,
|
||||||
|
flow_shift=float(flow_shift),
|
||||||
|
audio_flow_shift=float(audio_flow_shift),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def timestep_plans(contract: Mapping[str, Any]) -> list[torch.Tensor]:
|
||||||
|
"""Use the serving scheduler and its conditioning timesteps, without interpolation."""
|
||||||
|
from .denoise_loop import MINIMAX_H3_AUDIO_REF_COND_TIMESTEP, MINIMAX_H3_IMGVID_COND_TIMESTEP
|
||||||
|
|
||||||
|
video = minimax_h3_time_shift_sigmas(
|
||||||
|
num_steps=contract["num_steps"], shift_scale=contract["flow_shift"], base_schedule=contract["base_schedule"]
|
||||||
|
)
|
||||||
|
audio = minimax_h3_time_shift_sigmas(
|
||||||
|
num_steps=contract["num_steps"],
|
||||||
|
shift_scale=contract["audio_flow_shift"],
|
||||||
|
base_schedule=contract["base_schedule"],
|
||||||
|
)
|
||||||
|
plans: dict[tuple[float, ...], torch.Tensor] = {}
|
||||||
|
for video_sigma, audio_sigma in zip(video[:-1], audio[:-1], strict=True):
|
||||||
|
values = {
|
||||||
|
"video": 1.0 - video_sigma,
|
||||||
|
"audio": 1.0 - audio_sigma,
|
||||||
|
"image": max(1.0 - video_sigma, MINIMAX_H3_IMGVID_COND_TIMESTEP),
|
||||||
|
"audio_ref": max(1.0 - audio_sigma, MINIMAX_H3_AUDIO_REF_COND_TIMESTEP),
|
||||||
|
}
|
||||||
|
plan = torch.tensor([values[key] for key in MODES[contract["mode"]]], dtype=torch.float32).unique(sorted=True)
|
||||||
|
plans[tuple(plan.tolist())] = plan
|
||||||
|
return list(plans.values())
|
||||||
|
|
||||||
|
|
||||||
|
class MiniMaxH3AdalnCache(nn.Module):
|
||||||
|
"""Validate an optional sidecar, then verify its weights at load.
|
||||||
|
|
||||||
|
Hashes are checked against the *post-fusion* stream, before TP conversion.
|
||||||
|
They cover all inputs of the cached computation, not unrelated QKV/MLP
|
||||||
|
weights. A fixed adapter has an additional complete-file identity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
block_params: torch.Tensor
|
||||||
|
final_params: torch.Tensor
|
||||||
|
plan_timesteps: torch.Tensor
|
||||||
|
plan_lengths: torch.Tensor
|
||||||
|
time_embeddings: torch.Tensor
|
||||||
|
|
||||||
|
def __init__(self, arch: MiniMaxH3DiTArchConfig, *, path: str, model_variant: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.path = str(Path(path).expanduser().resolve())
|
||||||
|
self.arch = arch
|
||||||
|
self._verified: set[str] = set()
|
||||||
|
self._adapter_bound = False
|
||||||
|
self._ready = False
|
||||||
|
with safe_open(self.path, framework="pt", device="cpu") as source:
|
||||||
|
metadata = source.metadata() or {}
|
||||||
|
if metadata.get("format_version") != FORMAT_VERSION:
|
||||||
|
raise ValueError("Rebuild the AdaLN cache: a weight-bound format-v4 sidecar is required")
|
||||||
|
try:
|
||||||
|
manifest = json.loads(metadata["manifest"])
|
||||||
|
except (KeyError, TypeError, ValueError) as exc:
|
||||||
|
raise ValueError("Invalid AdaLN cache manifest") from exc
|
||||||
|
required = {"architecture", "model_variant", "schedule", "adapter_sha256", "weights", "payload", "math"}
|
||||||
|
if not isinstance(manifest, dict) or set(manifest) != required:
|
||||||
|
raise ValueError("Invalid AdaLN cache manifest fields")
|
||||||
|
if any(
|
||||||
|
not isinstance(manifest[key], dict)
|
||||||
|
for key in ("architecture", "schedule", "weights", "payload", "math")
|
||||||
|
):
|
||||||
|
raise ValueError("Invalid AdaLN cache manifest objects")
|
||||||
|
if manifest["architecture"] != architecture(arch) or manifest["model_variant"] != model_variant:
|
||||||
|
raise ValueError("AdaLN cache model variant or architecture mismatch")
|
||||||
|
contract = schedule_contract(**manifest["schedule"])
|
||||||
|
variant = "ref2va" if contract["mode"].startswith("ref2va") else "fl2va"
|
||||||
|
if variant != model_variant:
|
||||||
|
raise ValueError("AdaLN cache task does not match model variant")
|
||||||
|
if set(manifest["weights"]) != input_names(arch.num_layers):
|
||||||
|
raise ValueError("AdaLN cache must cover exactly the time embedding and AdaLN parameters")
|
||||||
|
if set(source.keys()) != PAYLOAD_NAMES or set(manifest["payload"]) != PAYLOAD_NAMES:
|
||||||
|
raise ValueError("Invalid AdaLN cache payload keys")
|
||||||
|
for digest in [*manifest["weights"].values(), *manifest["payload"].values()]:
|
||||||
|
if not isinstance(digest, str) or len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
|
||||||
|
raise ValueError("Invalid AdaLN cache SHA256 digest")
|
||||||
|
adapter = manifest["adapter_sha256"]
|
||||||
|
if adapter is not None and (
|
||||||
|
not isinstance(adapter, str) or len(adapter) != 64 or any(c not in "0123456789abcdef" for c in adapter)
|
||||||
|
):
|
||||||
|
raise ValueError("Invalid AdaLN cache adapter identity")
|
||||||
|
payload = {name: source.get_tensor(name) for name in PAYLOAD_NAMES}
|
||||||
|
plans = timestep_plans(contract)
|
||||||
|
width = max(len(plan) for plan in plans)
|
||||||
|
expected_lengths = torch.tensor([len(plan) for plan in plans], dtype=torch.int64)
|
||||||
|
expected_plans = torch.zeros((len(plans), width), dtype=torch.float32)
|
||||||
|
for index, plan in enumerate(plans):
|
||||||
|
expected_plans[index, : len(plan)] = plan
|
||||||
|
shapes = {
|
||||||
|
"block_params": ((len(plans), width, arch.num_layers, 18 * arch.hidden_size), torch.bfloat16),
|
||||||
|
"final_params": ((len(plans), width, 2 * arch.hidden_size), torch.bfloat16),
|
||||||
|
"time_embeddings": ((len(plans), width, arch.time_embed_dim), torch.float32),
|
||||||
|
"plan_timesteps": (tuple(expected_plans.shape), torch.float32),
|
||||||
|
"plan_lengths": (tuple(expected_lengths.shape), torch.int64),
|
||||||
|
}
|
||||||
|
for name, value in payload.items():
|
||||||
|
shape, dtype = shapes[name]
|
||||||
|
if tuple(value.shape) != shape or value.dtype != dtype or not bool(torch.isfinite(value).all()):
|
||||||
|
raise ValueError(f"Invalid AdaLN cache tensor: {name}")
|
||||||
|
if tensor_digest(value) != manifest["payload"][name]:
|
||||||
|
raise ValueError(f"AdaLN cache payload checksum mismatch: {name}")
|
||||||
|
if not torch.equal(payload["plan_timesteps"], expected_plans) or not torch.equal(
|
||||||
|
payload["plan_lengths"], expected_lengths
|
||||||
|
):
|
||||||
|
raise ValueError("AdaLN cache timestep plans do not match the declared schedule")
|
||||||
|
self.manifest = manifest
|
||||||
|
self.projection_names = projection_names(arch.num_layers)
|
||||||
|
# Index the validated CPU payload once. Runtime prepare() already hashes
|
||||||
|
# the actual embedding, so lookup needs no per-plan device comparisons.
|
||||||
|
self._embedding_plans = {
|
||||||
|
tensor_digest(payload["time_embeddings"][index, :count]): index
|
||||||
|
for index, count in enumerate(expected_lengths.tolist())
|
||||||
|
}
|
||||||
|
for name, value in payload.items():
|
||||||
|
self.register_buffer(name, value, persistent=False)
|
||||||
|
|
||||||
|
def bind_adapter(self, adapter_sha256: str | None) -> None:
|
||||||
|
if adapter_sha256 != self.manifest["adapter_sha256"]:
|
||||||
|
raise ValueError("AdaLN cache adapter mismatch; rebuild for the active fixed adapter")
|
||||||
|
self._adapter_bound = True
|
||||||
|
|
||||||
|
def verify_weight(self, name: str, weight: torch.Tensor) -> None:
|
||||||
|
if name not in self.manifest["weights"]:
|
||||||
|
return
|
||||||
|
if self._ready or name in self._verified:
|
||||||
|
raise ValueError(f"AdaLN input was loaded more than once: {name}")
|
||||||
|
if tensor_digest(weight) != self.manifest["weights"][name]:
|
||||||
|
raise ValueError(f"AdaLN cache effective weight mismatch: {name}")
|
||||||
|
self._verified.add(name)
|
||||||
|
|
||||||
|
def finish_loading(self, device: torch.device) -> None:
|
||||||
|
if not self._adapter_bound:
|
||||||
|
raise ValueError("AdaLN cache requires the active adapter identity before loading")
|
||||||
|
missing = input_names(self.arch.num_layers) - self._verified
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Missing AdaLN cache source weights: {sorted(missing)}")
|
||||||
|
if math_identity(device) != self.manifest["math"]:
|
||||||
|
raise ValueError("AdaLN cache numerical environment mismatch; rebuild on the serving configuration")
|
||||||
|
self.to(device=device)
|
||||||
|
self._ready = True
|
||||||
|
|
||||||
|
def check_request(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
mode: str,
|
||||||
|
num_steps: int,
|
||||||
|
base_schedule: Sequence[float] | None,
|
||||||
|
flow_shift: float,
|
||||||
|
audio_flow_shift: float,
|
||||||
|
) -> None:
|
||||||
|
contract = schedule_contract(
|
||||||
|
mode=mode,
|
||||||
|
num_steps=num_steps,
|
||||||
|
base_schedule=base_schedule,
|
||||||
|
flow_shift=flow_shift,
|
||||||
|
audio_flow_shift=audio_flow_shift,
|
||||||
|
)
|
||||||
|
if contract != self.manifest["schedule"]:
|
||||||
|
raise ValueError("AdaLN cache request schedule/task mismatch; rebuild or disable the cache")
|
||||||
|
|
||||||
|
def lookup(self, timesteps: torch.Tensor) -> tuple[tuple[tuple[torch.Tensor, ...], ...], tuple[torch.Tensor, ...]]:
|
||||||
|
if not self._ready:
|
||||||
|
raise RuntimeError("AdaLN cache has not verified its source weights")
|
||||||
|
if timesteps.dtype != torch.float32:
|
||||||
|
raise ValueError("AdaLN cache requires exact FP32 timesteps")
|
||||||
|
values = timesteps.reshape(-1).to(self.plan_timesteps.device)
|
||||||
|
count = values.numel()
|
||||||
|
if not 0 < count <= self.plan_timesteps.shape[1]:
|
||||||
|
raise ValueError("AdaLN cache does not cover this timestep plan")
|
||||||
|
matches = self.plan_lengths.eq(count) & self.plan_timesteps[:, :count].eq(values).all(dim=1)
|
||||||
|
if not bool(matches.any()):
|
||||||
|
raise ValueError("AdaLN cache does not cover this timestep plan")
|
||||||
|
index = matches.to(torch.int64).argmax()
|
||||||
|
blocks = self.block_params.permute(2, 0, 1, 3)[:, index, :count]
|
||||||
|
blocks = blocks.reshape(self.arch.num_layers, -1, 6, self.arch.hidden_size)
|
||||||
|
final = self.final_params[index, :count].reshape(-1, 2, self.arch.hidden_size)
|
||||||
|
return tuple(tuple(layer.unbind(1)) for layer in blocks), tuple(final.unbind(1))
|
||||||
|
|
||||||
|
|
||||||
|
@torch.inference_mode()
|
||||||
|
def build_cache(
|
||||||
|
arch: MiniMaxH3DiTArchConfig,
|
||||||
|
weights: Iterable[tuple[str, torch.Tensor]],
|
||||||
|
*,
|
||||||
|
contract: dict[str, Any],
|
||||||
|
model_variant: str,
|
||||||
|
adapter_sha256: str | None,
|
||||||
|
device: torch.device,
|
||||||
|
) -> tuple[dict[str, torch.Tensor], dict[str, Any]]:
|
||||||
|
"""Stream one projection at a time; do not instantiate the full DiT.
|
||||||
|
|
||||||
|
The four time-embedder tensors must precede the AdaLN projections. Callers
|
||||||
|
fuse any fixed adapter before supplying this stream, just like serving.
|
||||||
|
"""
|
||||||
|
from .minimax_h3_transformer import MiniMaxH3TimeEmbedder
|
||||||
|
|
||||||
|
contract = schedule_contract(**contract)
|
||||||
|
plans = timestep_plans(contract)
|
||||||
|
width = max(len(plan) for plan in plans)
|
||||||
|
payload = {
|
||||||
|
"plan_timesteps": torch.zeros((len(plans), width), dtype=torch.float32),
|
||||||
|
"plan_lengths": torch.tensor([len(plan) for plan in plans], dtype=torch.int64),
|
||||||
|
"block_params": torch.zeros((len(plans), width, arch.num_layers, 18 * arch.hidden_size), dtype=torch.bfloat16),
|
||||||
|
"final_params": torch.zeros((len(plans), width, 2 * arch.hidden_size), dtype=torch.bfloat16),
|
||||||
|
"time_embeddings": torch.zeros((len(plans), width, arch.time_embed_dim), dtype=torch.float32),
|
||||||
|
}
|
||||||
|
for i, plan in enumerate(plans):
|
||||||
|
payload["plan_timesteps"][i, : len(plan)] = plan
|
||||||
|
|
||||||
|
# Use the runtime embedding implementation, including FP32 boundaries. A
|
||||||
|
# lightweight module supplies ordinary Linear wrappers with TP1 semantics.
|
||||||
|
class Linear(nn.Linear):
|
||||||
|
def forward(self, x):
|
||||||
|
return super().forward(x), None
|
||||||
|
|
||||||
|
embedder = MiniMaxH3TimeEmbedder.__new__(MiniMaxH3TimeEmbedder)
|
||||||
|
nn.Module.__init__(embedder)
|
||||||
|
embedder.frequency_embedding_size = arch.timestep_input_dim
|
||||||
|
embedder.proj_in = Linear(arch.timestep_input_dim, arch.time_embed_hidden_size, device=device, dtype=torch.float32)
|
||||||
|
embedder.proj_out = Linear(arch.time_embed_hidden_size, arch.time_embed_dim, device=device, dtype=torch.float32)
|
||||||
|
fingerprints: dict[str, str] = {}
|
||||||
|
pending: dict[str, torch.Tensor] = {}
|
||||||
|
inputs: list[torch.Tensor] | None = None
|
||||||
|
for name, weight in weights:
|
||||||
|
if name not in input_names(arch.num_layers) or name in fingerprints:
|
||||||
|
raise ValueError(f"Unexpected or duplicate AdaLN builder input: {name}")
|
||||||
|
dtype = torch.float32 if name.startswith("time_embedder.") else torch.bfloat16
|
||||||
|
if weight.dtype != dtype or not bool(torch.isfinite(weight).all()):
|
||||||
|
raise ValueError(f"Invalid AdaLN builder input dtype/values: {name}")
|
||||||
|
fingerprints[name] = tensor_digest(weight)
|
||||||
|
if name.startswith("time_embedder."):
|
||||||
|
target = dict(embedder.named_parameters())[name.removeprefix("time_embedder.")]
|
||||||
|
if weight.shape != target.shape:
|
||||||
|
raise ValueError(f"Invalid AdaLN time embedding shape: {name}")
|
||||||
|
target.copy_(weight)
|
||||||
|
continue
|
||||||
|
if inputs is None:
|
||||||
|
if len([key for key in fingerprints if key.startswith("time_embedder.")]) != 4:
|
||||||
|
raise ValueError("Supply all four time embedding weights before the AdaLN projections")
|
||||||
|
embeddings = [embedder(plan.to(device)) for plan in plans]
|
||||||
|
for index, embedding in enumerate(embeddings):
|
||||||
|
payload["time_embeddings"][index, : len(embedding)].copy_(embedding.cpu())
|
||||||
|
inputs = [nn.functional.silu(embedding).to(torch.bfloat16) for embedding in embeddings]
|
||||||
|
pending[name] = weight.to(device)
|
||||||
|
prefix = name.rsplit(".", 1)[0]
|
||||||
|
if f"{prefix}.weight" not in pending or f"{prefix}.bias" not in pending:
|
||||||
|
continue
|
||||||
|
w, bias = pending.pop(f"{prefix}.weight"), pending.pop(f"{prefix}.bias")
|
||||||
|
is_final = prefix.startswith("final_layer.")
|
||||||
|
out_dim = (2 if is_final else 18) * arch.hidden_size
|
||||||
|
if w.shape != (out_dim, arch.time_embed_dim) or bias.shape != (out_dim,):
|
||||||
|
raise ValueError(f"Invalid AdaLN projection shape: {prefix}")
|
||||||
|
for i, x in enumerate(inputs):
|
||||||
|
result = nn.functional.linear(x, w, bias).cpu()
|
||||||
|
if is_final:
|
||||||
|
payload["final_params"][i, : len(x)].copy_(result)
|
||||||
|
else:
|
||||||
|
payload["block_params"][i, : len(x), int(prefix.split(".")[1])].copy_(result)
|
||||||
|
del w, bias
|
||||||
|
if set(fingerprints) != input_names(arch.num_layers) or pending:
|
||||||
|
raise ValueError("Incomplete AdaLN builder input weights")
|
||||||
|
if any(not bool(torch.isfinite(value).all()) for value in payload.values()):
|
||||||
|
raise ValueError("Non-finite AdaLN builder output")
|
||||||
|
manifest = dict(
|
||||||
|
architecture=architecture(arch),
|
||||||
|
model_variant=model_variant,
|
||||||
|
schedule=contract,
|
||||||
|
adapter_sha256=adapter_sha256,
|
||||||
|
weights=fingerprints,
|
||||||
|
payload={name: tensor_digest(value) for name, value in payload.items()},
|
||||||
|
math=math_identity(device),
|
||||||
|
)
|
||||||
|
return payload, manifest
|
||||||
@@ -55,6 +55,7 @@ from vllm_omni.diffusion.layers.rope import RotaryEmbedding
|
|||||||
from vllm_omni.diffusion.models.host_weight_contract import FinalLayoutModelContract
|
from vllm_omni.diffusion.models.host_weight_contract import FinalLayoutModelContract
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
|
from .adaln_cache import MiniMaxH3RuntimeAdalnCache
|
||||||
from .fasth3 import _resolve_native_target
|
from .fasth3 import _resolve_native_target
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -759,12 +760,15 @@ class MiniMaxH3AdalnProj(nn.Module):
|
|||||||
expand_ratio: int,
|
expand_ratio: int,
|
||||||
modality_num: int,
|
modality_num: int,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
|
adaln_cache: MiniMaxH3RuntimeAdalnCache | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if out_features != expand_ratio * arch.hidden_size * modality_num:
|
if out_features != expand_ratio * arch.hidden_size * modality_num:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"adaln out_features mismatch: {out_features} != {expand_ratio}*{arch.hidden_size}*{modality_num}"
|
f"adaln out_features mismatch: {out_features} != {expand_ratio}*{arch.hidden_size}*{modality_num}"
|
||||||
)
|
)
|
||||||
|
self._adaln_cache = adaln_cache
|
||||||
|
self._cache_name = prefix + ".linear"
|
||||||
self.expand_ratio = expand_ratio
|
self.expand_ratio = expand_ratio
|
||||||
self.modality_num = modality_num
|
self.modality_num = modality_num
|
||||||
self.hidden_size = arch.hidden_size
|
self.hidden_size = arch.hidden_size
|
||||||
@@ -780,8 +784,16 @@ class MiniMaxH3AdalnProj(nn.Module):
|
|||||||
|
|
||||||
def forward(self, t_emb: torch.Tensor) -> tuple[torch.Tensor, ...]:
|
def forward(self, t_emb: torch.Tensor) -> tuple[torch.Tensor, ...]:
|
||||||
"""t_emb: [M, t_dim] -> expand_ratio tensors of [M*modality_num, H]."""
|
"""t_emb: [M, t_dim] -> expand_ratio tensors of [M*modality_num, H]."""
|
||||||
x = nn.functional.silu(t_emb)
|
|
||||||
x, _ = self.linear(x.to(_BF16_DTYPE))
|
def project() -> torch.Tensor:
|
||||||
|
x = nn.functional.silu(t_emb)
|
||||||
|
return self.linear(x.to(_BF16_DTYPE))[0]
|
||||||
|
|
||||||
|
x = (
|
||||||
|
project()
|
||||||
|
if self._adaln_cache is None
|
||||||
|
else self._adaln_cache.project(self._cache_name, self.linear, t_emb, project)
|
||||||
|
)
|
||||||
m = x.shape[0]
|
m = x.shape[0]
|
||||||
x = x.view(m * self.modality_num, self.expand_ratio * self.hidden_size)
|
x = x.view(m * self.modality_num, self.expand_ratio * self.hidden_size)
|
||||||
return tuple(x.chunk(self.expand_ratio, dim=-1))
|
return tuple(x.chunk(self.expand_ratio, dim=-1))
|
||||||
@@ -877,6 +889,7 @@ class MiniMaxH3DiTBlock(nn.Module):
|
|||||||
quant_config: QuantizationConfig | None,
|
quant_config: QuantizationConfig | None,
|
||||||
*,
|
*,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
|
adaln_cache: MiniMaxH3RuntimeAdalnCache | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.norm1 = _norm(arch.hidden_size, eps=arch.norm_eps)
|
self.norm1 = _norm(arch.hidden_size, eps=arch.norm_eps)
|
||||||
@@ -900,6 +913,7 @@ class MiniMaxH3DiTBlock(nn.Module):
|
|||||||
expand_ratio=6,
|
expand_ratio=6,
|
||||||
modality_num=MINIMAX_H3_ADALN_MODALITY_NUM,
|
modality_num=MINIMAX_H3_ADALN_MODALITY_NUM,
|
||||||
prefix=f"{prefix}.adaln_proj",
|
prefix=f"{prefix}.adaln_proj",
|
||||||
|
adaln_cache=adaln_cache,
|
||||||
)
|
)
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
@@ -975,6 +989,7 @@ class MiniMaxH3FinalLayer(nn.Module):
|
|||||||
quant_config: QuantizationConfig | None,
|
quant_config: QuantizationConfig | None,
|
||||||
*,
|
*,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
|
adaln_cache: MiniMaxH3RuntimeAdalnCache | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
video_patch_dim = arch.latents_dim * arch.patch_size[0] * arch.patch_size[1] * arch.patch_size[2]
|
video_patch_dim = arch.latents_dim * arch.patch_size[0] * arch.patch_size[1] * arch.patch_size[2]
|
||||||
@@ -986,6 +1001,7 @@ class MiniMaxH3FinalLayer(nn.Module):
|
|||||||
expand_ratio=2,
|
expand_ratio=2,
|
||||||
modality_num=1,
|
modality_num=1,
|
||||||
prefix=f"{prefix}.adaln_proj",
|
prefix=f"{prefix}.adaln_proj",
|
||||||
|
adaln_cache=adaln_cache,
|
||||||
)
|
)
|
||||||
self.video_out = ColumnParallelLinear(
|
self.video_out = ColumnParallelLinear(
|
||||||
arch.hidden_size,
|
arch.hidden_size,
|
||||||
@@ -1164,6 +1180,15 @@ class MiniMaxH3DiTModel(nn.Module):
|
|||||||
self._rope_theta = float(config_mapping.get("rope_theta", 10000.0))
|
self._rope_theta = float(config_mapping.get("rope_theta", 10000.0))
|
||||||
arch = MiniMaxH3DiTArchConfig.from_mapping(config_mapping)
|
arch = MiniMaxH3DiTArchConfig.from_mapping(config_mapping)
|
||||||
self.arch = arch
|
self.arch = arch
|
||||||
|
cache_config = getattr(od_config, "cache_config", {})
|
||||||
|
enabled = (
|
||||||
|
cache_config.get("minimax_h3_adaln_cache", True)
|
||||||
|
if isinstance(cache_config, Mapping)
|
||||||
|
else getattr(cache_config, "minimax_h3_adaln_cache", True)
|
||||||
|
)
|
||||||
|
if type(enabled) is not bool:
|
||||||
|
raise ValueError("minimax_h3_adaln_cache must be a boolean")
|
||||||
|
self.adaln_cache = MiniMaxH3RuntimeAdalnCache(max_bytes=256 * 1024**2 if enabled else 0)
|
||||||
self.od_config = od_config
|
self.od_config = od_config
|
||||||
self.parallel_config = od_config.parallel_config
|
self.parallel_config = od_config.parallel_config
|
||||||
self.hidden_size = arch.hidden_size
|
self.hidden_size = arch.hidden_size
|
||||||
@@ -1227,6 +1252,7 @@ class MiniMaxH3DiTModel(nn.Module):
|
|||||||
arch,
|
arch,
|
||||||
quant_config,
|
quant_config,
|
||||||
prefix=f"blocks.{i}",
|
prefix=f"blocks.{i}",
|
||||||
|
adaln_cache=self.adaln_cache,
|
||||||
)
|
)
|
||||||
for i in range(arch.num_layers)
|
for i in range(arch.num_layers)
|
||||||
]
|
]
|
||||||
@@ -1239,6 +1265,7 @@ class MiniMaxH3DiTModel(nn.Module):
|
|||||||
arch,
|
arch,
|
||||||
quant_config,
|
quant_config,
|
||||||
prefix="final_layer",
|
prefix="final_layer",
|
||||||
|
adaln_cache=self.adaln_cache,
|
||||||
)
|
)
|
||||||
self._mark_missing_params_required()
|
self._mark_missing_params_required()
|
||||||
|
|
||||||
@@ -1311,6 +1338,12 @@ class MiniMaxH3DiTModel(nn.Module):
|
|||||||
if rope_table.dtype != _BF16_DTYPE:
|
if rope_table.dtype != _BF16_DTYPE:
|
||||||
raise ValueError(f"rope_table must be {_BF16_DTYPE}, got {rope_table.dtype}.")
|
raise ValueError(f"rope_table must be {_BF16_DTYPE}, got {rope_table.dtype}.")
|
||||||
|
|
||||||
|
def _apply(self, fn, recurse=True):
|
||||||
|
# Derived outputs must not keep the old device alive after offload/move.
|
||||||
|
if getattr(self, "adaln_cache", None) is not None:
|
||||||
|
self.adaln_cache.clear()
|
||||||
|
return super()._apply(fn, recurse=recurse)
|
||||||
|
|
||||||
def post_load_weights(self) -> None:
|
def post_load_weights(self) -> None:
|
||||||
for name, param in self.named_parameters():
|
for name, param in self.named_parameters():
|
||||||
if name in MINIMAX_H3_FP32_PARAM_NAMES and param.dtype != _FP32_DTYPE:
|
if name in MINIMAX_H3_FP32_PARAM_NAMES and param.dtype != _FP32_DTYPE:
|
||||||
@@ -1328,6 +1361,8 @@ class MiniMaxH3DiTModel(nn.Module):
|
|||||||
weights: Iterable[tuple[str, torch.Tensor]],
|
weights: Iterable[tuple[str, torch.Tensor]],
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
"""Load native or Diffusers H3 weights with the existing TP-aware loaders."""
|
"""Load native or Diffusers H3 weights with the existing TP-aware loaders."""
|
||||||
|
if getattr(self, "adaln_cache", None) is not None:
|
||||||
|
self.adaln_cache.clear()
|
||||||
params = dict(self.named_parameters())
|
params = dict(self.named_parameters())
|
||||||
params.update(dict(self.named_buffers()))
|
params.update(dict(self.named_buffers()))
|
||||||
loaded: set[str] = set()
|
loaded: set[str] = set()
|
||||||
@@ -1606,6 +1641,9 @@ class MiniMaxH3DiTModel(nn.Module):
|
|||||||
local_span=local_span,
|
local_span=local_span,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if getattr(self, "adaln_cache", None) is not None:
|
||||||
|
self.adaln_cache.prepare(t_emb)
|
||||||
|
|
||||||
combined_indices = (inverse_indices * MINIMAX_H3_ADALN_MODALITY_NUM + token_tags.clamp(min=0)).to(device)
|
combined_indices = (inverse_indices * MINIMAX_H3_ADALN_MODALITY_NUM + token_tags.clamp(min=0)).to(device)
|
||||||
inverse_indices = inverse_indices.to(device)
|
inverse_indices = inverse_indices.to(device)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ from vllm_omni.diffusion.sched.sigma_schedule import DMD2SigmaSchedule
|
|||||||
from vllm_omni.diffusion.utils.media_utils import normalize_preencode_batch_frames, normalize_video_codec_options
|
from vllm_omni.diffusion.utils.media_utils import normalize_preencode_batch_frames, normalize_video_codec_options
|
||||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||||
from vllm_omni.errors import OmniClientError, client_error_from_metadata
|
from vllm_omni.errors import OmniClientError, client_error_from_metadata
|
||||||
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||||
from vllm_omni.model_executor.model_loader.weight_utils import (
|
from vllm_omni.model_executor.model_loader.weight_utils import (
|
||||||
download_weights_from_hf_specific,
|
download_weights_from_hf_specific,
|
||||||
)
|
)
|
||||||
@@ -642,6 +643,7 @@ class MiniMaxH3Pipeline(
|
|||||||
) -> tuple[LoRAModel, PEFTHelper] | None:
|
) -> tuple[LoRAModel, PEFTHelper] | None:
|
||||||
# A cache eviction may be followed by a different adapter reusing the
|
# A cache eviction may be followed by a different adapter reusing the
|
||||||
# same client-supplied ID. Every real load replaces the classification.
|
# same client-supplied ID. Every real load replaces the classification.
|
||||||
|
self._clear_adaln_caches()
|
||||||
self._turbo_lora_specs.pop(lora_request.lora_int_id, None)
|
self._turbo_lora_specs.pop(lora_request.lora_int_id, None)
|
||||||
self._native_lora_adapter_ids.discard(lora_request.lora_int_id)
|
self._native_lora_adapter_ids.discard(lora_request.lora_int_id)
|
||||||
self._lora_sigma_schedules.pop(lora_request.lora_int_id, None)
|
self._lora_sigma_schedules.pop(lora_request.lora_int_id, None)
|
||||||
@@ -975,6 +977,22 @@ class MiniMaxH3Pipeline(
|
|||||||
audio_shift=self.default_audio_shift,
|
audio_shift=self.default_audio_shift,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._configure_adaln_sidecar(
|
||||||
|
self.transformer,
|
||||||
|
"minimax_h3_adaln_cache_path",
|
||||||
|
expected_partition,
|
||||||
|
self._fasth3.source if self._fasth3 is not None else None,
|
||||||
|
eligible=transformer_quant_config is None and not modular,
|
||||||
|
)
|
||||||
|
if ref2va_model_path is not None:
|
||||||
|
self._configure_adaln_sidecar(
|
||||||
|
self.transformers_ref,
|
||||||
|
"minimax_h3_ref_adaln_cache_path",
|
||||||
|
"ref2va",
|
||||||
|
None,
|
||||||
|
eligible=transformer_quant_config is None and not modular,
|
||||||
|
)
|
||||||
|
|
||||||
if self.load_text_encoder:
|
if self.load_text_encoder:
|
||||||
self.tokenizer = Qwen2TokenizerFast.from_pretrained(
|
self.tokenizer = Qwen2TokenizerFast.from_pretrained(
|
||||||
str(model_path),
|
str(model_path),
|
||||||
@@ -1103,11 +1121,14 @@ class MiniMaxH3Pipeline(
|
|||||||
# Fuse before the model shards anything, which is also the only
|
# Fuse before the model shards anything, which is also the only
|
||||||
# point where the checkpoint's fused QKV/MLP layouts are intact.
|
# point where the checkpoint's fused QKV/MLP layouts are intact.
|
||||||
stream = self._fasth3.apply(stream)
|
stream = self._fasth3.apply(stream)
|
||||||
|
if getattr(component, "_adaln_sidecar_candidate", None) is not None:
|
||||||
|
stream = self._verify_adaln_weights(component, stream)
|
||||||
loaded = component.load_weights(stream)
|
loaded = component.load_weights(stream)
|
||||||
if prefix == "transformer.":
|
if prefix == "transformer.":
|
||||||
transformer_loaded = set(loaded)
|
transformer_loaded = set(loaded)
|
||||||
if prefix != "text_encoder.":
|
if prefix != "text_encoder.":
|
||||||
component.post_load_weights()
|
component.post_load_weights()
|
||||||
|
self._finish_adaln_sidecar(component)
|
||||||
loaded_with_prefix.update(prefix + name for name in loaded)
|
loaded_with_prefix.update(prefix + name for name in loaded)
|
||||||
# Both VAEs load eagerly in ``__init__`` rather than through
|
# Both VAEs load eagerly in ``__init__`` rather than through
|
||||||
# ``weights_sources``. The text encoder uses the shared component
|
# ``weights_sources``. The text encoder uses the shared component
|
||||||
@@ -1135,6 +1156,89 @@ class MiniMaxH3Pipeline(
|
|||||||
"""True when --lora-path was consumed as a load-time weight fusion."""
|
"""True when --lora-path was consumed as a load-time weight fusion."""
|
||||||
return self._fasth3 is not None
|
return self._fasth3 is not None
|
||||||
|
|
||||||
|
def _configure_adaln_sidecar(
|
||||||
|
self,
|
||||||
|
transformer: MiniMaxH3DiTModel,
|
||||||
|
key: str,
|
||||||
|
variant: str,
|
||||||
|
adapter_path: str | Path | None,
|
||||||
|
*,
|
||||||
|
eligible: bool,
|
||||||
|
) -> None:
|
||||||
|
from safetensors import SafetensorError
|
||||||
|
from vllm.distributed import get_tensor_model_parallel_world_size
|
||||||
|
|
||||||
|
from .adaln_cache import MiniMaxH3AdalnCache, file_digest
|
||||||
|
|
||||||
|
config = self.od_config.cache_config
|
||||||
|
path = config.get(key) if isinstance(config, Mapping) else getattr(config, key, None)
|
||||||
|
if path is None or not transformer.adaln_cache.max_bytes:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# Compiled blocks bypass projection reuse. Reject before reading the
|
||||||
|
# sidecar so load completion cannot move an unused payload to GPU.
|
||||||
|
if not self.od_config.enforce_eager:
|
||||||
|
raise ValueError(
|
||||||
|
"offline sidecars require --enforce-eager; compiled H3 blocks bypass cached projections"
|
||||||
|
)
|
||||||
|
if not eligible or get_tensor_model_parallel_world_size() != 1:
|
||||||
|
raise ValueError("offline sidecar uses native BF16 TP1 math; use the default runtime cache here")
|
||||||
|
sidecar = MiniMaxH3AdalnCache(transformer.arch, path=path, model_variant=variant)
|
||||||
|
sidecar.bind_adapter(file_digest(adapter_path) if adapter_path is not None else None)
|
||||||
|
# Keep optional CPU-derived data outside the registered model tree.
|
||||||
|
object.__setattr__(transformer, "_adaln_sidecar_candidate", sidecar)
|
||||||
|
except (OSError, RuntimeError, TypeError, ValueError, SafetensorError) as exc:
|
||||||
|
logger.warning("Rejecting optional H3 AdaLN sidecar; runtime caching remains enabled: %s", exc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _verify_adaln_weights(
|
||||||
|
component: MiniMaxH3DiTModel, weights: Iterable[tuple[str, torch.Tensor]]
|
||||||
|
) -> Iterable[tuple[str, torch.Tensor]]:
|
||||||
|
for name, tensor in weights:
|
||||||
|
candidate = getattr(component, "_adaln_sidecar_candidate", None)
|
||||||
|
if candidate is not None:
|
||||||
|
try:
|
||||||
|
candidate.verify_weight(name, tensor)
|
||||||
|
except ValueError as exc:
|
||||||
|
object.__setattr__(component, "_adaln_sidecar_candidate", None)
|
||||||
|
logger.warning("Rejecting optional H3 AdaLN sidecar: %s", exc)
|
||||||
|
yield name, tensor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _finish_adaln_sidecar(component: nn.Module) -> None:
|
||||||
|
from vllm.model_executor.layers.utils import default_unquantized_gemm
|
||||||
|
|
||||||
|
candidate = getattr(component, "_adaln_sidecar_candidate", None)
|
||||||
|
if candidate is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
modules = {
|
||||||
|
f"blocks.{i}.adaln_proj.linear": block.adaln_proj.linear for i, block in enumerate(component.blocks)
|
||||||
|
}
|
||||||
|
modules["final_layer.adaln_proj.linear"] = component.final_layer.adaln_proj.linear
|
||||||
|
for module in (*modules.values(), component.time_embedder.proj_in, component.time_embedder.proj_out):
|
||||||
|
if getattr(module.quant_method, "_gemm_impl", None) is not default_unquantized_gemm:
|
||||||
|
raise ValueError("offline sidecar requires the builder's torch linear backend")
|
||||||
|
candidate.finish_loading(component.video_patch_proj.weight.device)
|
||||||
|
component.adaln_cache.seed(candidate, modules)
|
||||||
|
except (RuntimeError, ValueError) as exc:
|
||||||
|
logger.warning("Rejecting optional H3 AdaLN sidecar; using runtime projections: %s", exc)
|
||||||
|
finally:
|
||||||
|
object.__setattr__(component, "_adaln_sidecar_candidate", None)
|
||||||
|
|
||||||
|
def _clear_adaln_caches(self) -> None:
|
||||||
|
for name in ("transformer", "transformers_ref"):
|
||||||
|
cache = getattr(getattr(self, name, None), "adaln_cache", None)
|
||||||
|
if cache is not None:
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
def _prepare_adaln_adapter(self, sampling: OmniDiffusionSamplingParams) -> None:
|
||||||
|
request = getattr(sampling, "lora_request", None)
|
||||||
|
identity = None if request is None else (request.lora_int_id, request.lora_path, float(sampling.lora_scale))
|
||||||
|
if identity != getattr(self, "_adaln_adapter_identity", None):
|
||||||
|
self._clear_adaln_caches()
|
||||||
|
self._adaln_adapter_identity = identity
|
||||||
|
|
||||||
def _transformer_for_task(self, task: str) -> MiniMaxH3DiTModel:
|
def _transformer_for_task(self, task: str) -> MiniMaxH3DiTModel:
|
||||||
if task == "ref2va" and hasattr(self, "transformers_ref"):
|
if task == "ref2va" and hasattr(self, "transformers_ref"):
|
||||||
return self.transformers_ref
|
return self.transformers_ref
|
||||||
@@ -2431,7 +2535,27 @@ class MiniMaxH3Pipeline(
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise OmniClientError(str(exc)) from exc
|
raise OmniClientError(str(exc)) from exc
|
||||||
|
|
||||||
|
self._prepare_adaln_adapter(sampling)
|
||||||
base_schedule, num_steps = self._resolve_sigma_positions(task, sampling)
|
base_schedule, num_steps = self._resolve_sigma_positions(task, sampling)
|
||||||
|
transformer = getattr(
|
||||||
|
self, "transformers_ref" if task == "ref2va" and hasattr(self, "transformers_ref") else "transformer", None
|
||||||
|
)
|
||||||
|
cache = getattr(transformer, "adaln_cache", None)
|
||||||
|
if cache is not None and cache.sidecar is not None:
|
||||||
|
mode = task
|
||||||
|
if task == "ref2va":
|
||||||
|
mode += "-mixed" if visual_shapes and audio_lengths else "-audio" if audio_lengths else "-image"
|
||||||
|
try:
|
||||||
|
cache.sidecar.check_request(
|
||||||
|
mode=mode,
|
||||||
|
num_steps=num_steps,
|
||||||
|
base_schedule=base_schedule,
|
||||||
|
flow_shift=float(extra.get("flow_shift", self.default_video_shift)),
|
||||||
|
audio_flow_shift=float(extra.get("audio_flow_shift", self.default_audio_shift)),
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
logger.warning("Rejecting optional AdaLN sidecar for this schedule; using runtime cache: %s", exc)
|
||||||
|
cache.clear()
|
||||||
quality_plan = self._quality_policy.resolve(
|
quality_plan = self._quality_policy.resolve(
|
||||||
quality=sampling.quality,
|
quality=sampling.quality,
|
||||||
num_inference_steps=num_steps,
|
num_inference_steps=num_steps,
|
||||||
|
|||||||
Reference in new issue
Block a user