[Performance][MiniCPM-o] Whole-Euler CUDA graphs for Code2Wav with a shared attention arena (#8007)
This commit is contained in:
1 parent
0173cb3748
commit
3e62bba3cb
15 files changed
+3425
-210
No files matched your search
@@ -54,7 +54,7 @@ vllm-omni serve openbmb/MiniCPM-o-4_5 \
|
|||||||
```
|
```
|
||||||
|
|
||||||
`vllm_omni/deploy/minicpmo_4_5.yaml` declares `session_mode: duplex` and
|
`vllm_omni/deploy/minicpmo_4_5.yaml` declares `session_mode: duplex` and
|
||||||
`duplex_session.max_sessions: 4`. Because the MiniCPM-o 4.5 pipeline declares a
|
`duplex_session.max_sessions: 16`. Because the MiniCPM-o 4.5 pipeline declares a
|
||||||
`duplex_plugin`, `vllm-omni serve` runs it through `DuplexOmni`: the server
|
`duplex_plugin`, `vllm-omni serve` runs it through `DuplexOmni`: the server
|
||||||
mounts `ws://<host>:8099/v1/realtime?duplex=1` (this page; `ws://<host>:8099/v1/duplex`
|
mounts `ws://<host>:8099/v1/realtime?duplex=1` (this page; `ws://<host>:8099/v1/duplex`
|
||||||
is an alias of the same route), `POST /v1/chat/completions`, `/v1/models` and
|
is an alias of the same route), `POST /v1/chat/completions`, `/v1/models` and
|
||||||
|
|||||||
@@ -1408,12 +1408,12 @@ class TestDeployConfigLoading:
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("filename", "max_sessions"),
|
("filename", "max_sessions"),
|
||||||
[
|
[
|
||||||
("minicpmo_4_5.yaml", 4),
|
("minicpmo_4_5.yaml", 16),
|
||||||
("minicpmo_4_5_2gpu.yaml", 4),
|
("minicpmo_4_5_2gpu.yaml", 4),
|
||||||
("minicpmo_4_5_3gpu.yaml", 4),
|
("minicpmo_4_5_3gpu.yaml", 4),
|
||||||
("minicpmo_4_5_8x4090.yaml", 1),
|
("minicpmo_4_5_8x4090.yaml", 1),
|
||||||
("minicpmo_4_5_3gpu_stage1_replicas.yaml", 4),
|
("minicpmo_4_5_3gpu_stage1_replicas.yaml", 16),
|
||||||
("minicpmo_4_5_4gpu_stage1_replicas.yaml", 4),
|
("minicpmo_4_5_4gpu_stage1_replicas.yaml", 16),
|
||||||
("minicpmo_4_5_8x4090_stage1_replicas.yaml", 1),
|
("minicpmo_4_5_8x4090_stage1_replicas.yaml", 1),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -2886,7 +2886,7 @@ class TestPlatformOverrides:
|
|||||||
|
|
||||||
cuda = _apply_platform_overrides(load_deploy_config(deploy_path), platform="cuda")
|
cuda = _apply_platform_overrides(load_deploy_config(deploy_path), platform="cuda")
|
||||||
cuda_stages = merge_pipeline_deploy(pipeline, cuda)
|
cuda_stages = merge_pipeline_deploy(pipeline, cuda)
|
||||||
assert cuda_stages[1].yaml_engine_args["kv_cache_memory_bytes"] == 2 * 1024**3
|
assert cuda_stages[1].yaml_engine_args["kv_cache_memory_bytes"] == 4 * 1024**3
|
||||||
|
|
||||||
# The CUDA memory budget must not leak into the existing NPU profile.
|
# The CUDA memory budget must not leak into the existing NPU profile.
|
||||||
npu = _apply_platform_overrides(load_deploy_config(deploy_path), platform="npu")
|
npu = _apply_platform_overrides(load_deploy_config(deploy_path), platform="npu")
|
||||||
@@ -2903,7 +2903,7 @@ class TestPlatformOverrides:
|
|||||||
load_deploy_config(Path(get_deploy_config_path(filename))), platform="cuda"
|
load_deploy_config(Path(get_deploy_config_path(filename))), platform="cuda"
|
||||||
)
|
)
|
||||||
replica_stages = merge_pipeline_deploy(pipeline, replica)
|
replica_stages = merge_pipeline_deploy(pipeline, replica)
|
||||||
# Explicit null clears the inherited single-GPU 2 GiB CUDA cap.
|
# Explicit null clears the inherited single-GPU 4 GiB CUDA cap.
|
||||||
assert replica_stages[1].yaml_engine_args.get("kv_cache_memory_bytes") is None
|
assert replica_stages[1].yaml_engine_args.get("kv_cache_memory_bytes") is None
|
||||||
|
|
||||||
def test_fish_speech_npu_uses_ascend_kv_block_size(self):
|
def test_fish_speech_npu_uses_ascend_kv_block_size(self):
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ def test_duplex_session_runtime_rejects_non_positive_values(tmp_path, name: str,
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("deploy_yaml", "expected"),
|
("deploy_yaml", "expected"),
|
||||||
[
|
[
|
||||||
("minicpmo_4_5.yaml", 4),
|
("minicpmo_4_5.yaml", 16),
|
||||||
("minicpmo_4_5_8x4090.yaml", 1),
|
("minicpmo_4_5_8x4090.yaml", 1),
|
||||||
("minicpmo_4_5_3gpu_stage1_replicas.yaml", 4),
|
("minicpmo_4_5_3gpu_stage1_replicas.yaml", 16),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_deploy_duplex_max_sessions_tracks_the_deploy_config(deploy_yaml: str, expected: int) -> None:
|
def test_deploy_duplex_max_sessions_tracks_the_deploy_config(deploy_yaml: str, expected: int) -> None:
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ import torch
|
|||||||
|
|
||||||
from vllm_omni.model_executor.models.minicpmo_4_5.batched_token2wav import (
|
from vllm_omni.model_executor.models.minicpmo_4_5.batched_token2wav import (
|
||||||
_cfm_pad_frames,
|
_cfm_pad_frames,
|
||||||
_zero_padded_cnn_cache,
|
|
||||||
_zero_padded_frames,
|
_zero_padded_frames,
|
||||||
)
|
)
|
||||||
|
from vllm_omni.model_executor.models.minicpmo_4_5.cuda_graph_wrapper import (
|
||||||
|
_att_keep_ranges,
|
||||||
|
_build_capture_mask,
|
||||||
|
_capture_query_width,
|
||||||
|
_whole_euler_att_segments,
|
||||||
|
_zero_padded_cnn_cache,
|
||||||
|
)
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||||
|
|
||||||
@@ -139,6 +145,75 @@ def test_varied_chunk_lengths_collapse_onto_few_widths():
|
|||||||
assert len(padded) < len(varied)
|
assert len(padded) < len(varied)
|
||||||
|
|
||||||
|
|
||||||
|
def test_capture_query_width_collapses_decode_grid_onto_one_bucket():
|
||||||
|
"""Whole-Euler capture bucket is the #7416 pad helper with a 64-wide decode grid."""
|
||||||
|
assert _capture_query_width(16, 0) == 16
|
||||||
|
assert _capture_query_width(16, 1) == 16
|
||||||
|
assert {_capture_query_width(w, 64) for w in (16, 32, 48, 50, 64)} == {64}
|
||||||
|
assert _capture_query_width(304, 64) == 320
|
||||||
|
assert _capture_query_width(320, 64) == 320
|
||||||
|
|
||||||
|
|
||||||
|
def test_att_keep_ranges_match_the_streaming_trim():
|
||||||
|
"""Past ``prompt_len + 100`` frames the cache keeps its first ``prompt_len`` and last 100."""
|
||||||
|
assert _att_keep_ranges(368, (300, 100)) == [(0, 368)]
|
||||||
|
assert _att_keep_ranges(400, (300, 100)) == [(0, 400)]
|
||||||
|
assert _att_keep_ranges(464, (300, 100)) == [(0, 300), (364, 100)]
|
||||||
|
assert _att_keep_ranges(464, None) == [(0, 464)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_whole_euler_att_segments_skip_capture_padding():
|
||||||
|
# Steady chunk: capture width == chunk width, so only the trim applies.
|
||||||
|
assert _whole_euler_att_segments(mel_width=64, query_cap=64, offset=400, keep=(300, 100)) == [
|
||||||
|
(0, 300),
|
||||||
|
(364, 100),
|
||||||
|
]
|
||||||
|
# A 32-wide chunk on the 64 capture: the graph wrote [current 64 | cache 400];
|
||||||
|
# frames 32:64 are capture padding and the cache starts at 64.
|
||||||
|
assert _whole_euler_att_segments(mel_width=32, query_cap=64, offset=400, keep=None) == [(0, 32), (64, 400)]
|
||||||
|
assert _whole_euler_att_segments(mel_width=32, query_cap=64, offset=400, keep=(300, 100)) == [
|
||||||
|
(0, 32),
|
||||||
|
(64, 268),
|
||||||
|
(364, 100),
|
||||||
|
]
|
||||||
|
# First chunk: no cache, only the tail of the capture width is dropped.
|
||||||
|
assert _whole_euler_att_segments(mel_width=304, query_cap=320, offset=0, keep=None) == [(0, 304)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_capture_mask_moves_the_cache_block_past_capture_padding():
|
||||||
|
width, cap, offset, valid = 32, 64, 8, 30
|
||||||
|
caller = torch.ones(2, width, width + offset, dtype=torch.bool)
|
||||||
|
caller[:, :, valid:width] = False
|
||||||
|
mask = _build_capture_mask(
|
||||||
|
attn_mask=caller,
|
||||||
|
batch_size=1,
|
||||||
|
query_cap=cap,
|
||||||
|
offset=offset,
|
||||||
|
mel_width=width,
|
||||||
|
mel_frames=valid,
|
||||||
|
device=torch.device("cpu"),
|
||||||
|
)
|
||||||
|
assert mask.shape == (2, cap, cap + offset)
|
||||||
|
assert mask[:, :, :valid].all()
|
||||||
|
assert not mask[:, :, valid:cap].any()
|
||||||
|
assert mask[:, :, cap:].all()
|
||||||
|
# Capture-padding query rows are never empty, so SDPA cannot produce NaN there.
|
||||||
|
assert mask.any(dim=-1).all()
|
||||||
|
|
||||||
|
implicit = _build_capture_mask(
|
||||||
|
attn_mask=None,
|
||||||
|
batch_size=1,
|
||||||
|
query_cap=cap,
|
||||||
|
offset=offset,
|
||||||
|
mel_width=width,
|
||||||
|
mel_frames=width,
|
||||||
|
device=torch.device("cpu"),
|
||||||
|
)
|
||||||
|
assert implicit[:, :, :width].all()
|
||||||
|
assert not implicit[:, :, width:cap].any()
|
||||||
|
assert implicit[:, :, cap:].all()
|
||||||
|
|
||||||
|
|
||||||
def test_zero_padded_frames_survives_an_integration_step():
|
def test_zero_padded_frames_survives_an_integration_step():
|
||||||
"""The padded region must stay zero after ``x = x + dt * velocity``.
|
"""The padded region must stay zero after ``x = x + dt * velocity``.
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,19 @@ def _config(minimum: int = 1, initial: int = 0, *, runtime_prompt_cache_size: in
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("extra", "max_num_seqs", "micro"), [({}, 6, 6), ({}, 64, 16), ({"micro_batch_size": 2}, 6, 2)]
|
||||||
|
)
|
||||||
|
def test_whole_euler_micro_batch_defaults_to_stage_concurrency(extra, max_num_seqs, micro):
|
||||||
|
config = _config()
|
||||||
|
config.model_config.stage_connector_config["extra"].update(extra)
|
||||||
|
config.scheduler_config = SimpleNamespace(max_num_seqs=max_num_seqs)
|
||||||
|
|
||||||
|
model = MiniCPMO45Code2Wav(vllm_config=config)
|
||||||
|
|
||||||
|
assert model._cfm_graph_config["micro_batch_size"] == micro
|
||||||
|
|
||||||
|
|
||||||
def _model(initial: int = 0, minimum: int = 1, *, runtime_prompt_cache_size: int = 4, setup_cache_size: int = 1):
|
def _model(initial: int = 0, minimum: int = 1, *, runtime_prompt_cache_size: int = 4, setup_cache_size: int = 1):
|
||||||
token2wav = _FakeToken2Wav()
|
token2wav = _FakeToken2Wav()
|
||||||
backend = BatchedToken2Wav(token2wav, setup_cache_size=setup_cache_size)
|
backend = BatchedToken2Wav(token2wav, setup_cache_size=setup_cache_size)
|
||||||
@@ -213,17 +226,43 @@ def test_setup_cache_reuses_read_only_state_for_the_same_exact_batch():
|
|||||||
torch.testing.assert_close(cache[name], expected)
|
torch.testing.assert_close(cache[name], expected)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_cache_keys_batch_size_and_evicts_least_recent_entry():
|
def test_setup_solves_the_prompt_once_for_every_batch_size():
|
||||||
token2wav = _FakeToken2Wav()
|
token2wav = _FakeToken2Wav()
|
||||||
adapter = BatchedToken2Wav(token2wav, setup_cache_size=1)
|
adapter = BatchedToken2Wav(token2wav, setup_cache_size=1)
|
||||||
prompt = adapter.prepare_prompt("shared", "/fake/prompt.wav")
|
prompt = adapter.prepare_prompt("shared", "/fake/prompt.wav")
|
||||||
|
|
||||||
adapter.setup_batch(prompt, 1)
|
single = adapter.setup_batch(prompt, 1)
|
||||||
adapter.setup_batch(prompt, 2)
|
pair = adapter.setup_batch(prompt, 2)
|
||||||
adapter.setup_batch(prompt, 1)
|
|
||||||
|
|
||||||
assert token2wav.flow.encoder.calls == [1, 2, 1]
|
assert token2wav.flow.encoder.calls == [1]
|
||||||
assert list(adapter._setup_cache) == [(prompt.cache_key, 1, 0)]
|
assert pair == [single[0], single[0]]
|
||||||
|
assert list(adapter._setup_cache) == [(prompt.cache_key, 0)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_cache_evicts_least_recent_prompt():
|
||||||
|
token2wav = _FakeToken2Wav()
|
||||||
|
adapter = BatchedToken2Wav(token2wav, setup_cache_size=1)
|
||||||
|
first = adapter.prepare_prompt("first", "/fake/first.wav")
|
||||||
|
second = adapter.prepare_prompt("second", "/fake/second.wav")
|
||||||
|
|
||||||
|
adapter.setup_batch(first, 1)
|
||||||
|
adapter.setup_batch(second, 2)
|
||||||
|
adapter.setup_batch(first, 1)
|
||||||
|
|
||||||
|
assert token2wav.flow.encoder.calls == [1, 1, 1]
|
||||||
|
assert list(adapter._setup_cache) == [(first.cache_key, 0)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_without_cache_still_solves_one_shared_row():
|
||||||
|
token2wav = _FakeToken2Wav()
|
||||||
|
adapter = BatchedToken2Wav(token2wav, setup_cache_size=0)
|
||||||
|
prompt = adapter.prepare_prompt("shared", "/fake/prompt.wav")
|
||||||
|
|
||||||
|
states = adapter.setup_batch(prompt, 3)
|
||||||
|
|
||||||
|
assert token2wav.flow.encoder.calls == [1]
|
||||||
|
assert states[0] is states[1] is states[2]
|
||||||
|
assert not adapter._setup_cache
|
||||||
|
|
||||||
|
|
||||||
def test_evict_prompt_clears_features_and_setup_state():
|
def test_evict_prompt_clears_features_and_setup_state():
|
||||||
@@ -481,9 +520,12 @@ def test_adapter_runs_true_batch_cfg_and_splits_request_caches():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert token2wav.prompt_calls == 1
|
assert token2wav.prompt_calls == 1
|
||||||
assert token2wav.flow.encoder.calls == [2, 2]
|
# Setup solves the shared prompt once; the chunk runs both requests together.
|
||||||
assert token2wav.flow.decoder.estimator.cfg_batches == [4, 4, 4, 4]
|
assert token2wav.flow.encoder.calls == [1, 2]
|
||||||
assert all(order == [1.0, 1.0, 0.0, 0.0] for order in token2wav.flow.decoder.estimator.speaker_order)
|
assert token2wav.flow.decoder.estimator.cfg_batches == [2, 2, 4, 4]
|
||||||
|
speaker_order = token2wav.flow.decoder.estimator.speaker_order
|
||||||
|
assert all(order == [1.0, 0.0] for order in speaker_order[:2])
|
||||||
|
assert all(order == [1.0, 1.0, 0.0, 0.0] for order in speaker_order[2:])
|
||||||
assert token2wav.hift.calls == [2]
|
assert token2wav.hift.calls == [2]
|
||||||
assert len(audios) == 2
|
assert len(audios) == 2
|
||||||
cache0 = states[0].flow_cache["estimator_cnn_cache"]
|
cache0 = states[0].flow_cache["estimator_cnn_cache"]
|
||||||
@@ -706,6 +748,40 @@ def test_decode_cfm_enters_platform_sdpa_context(monkeypatch):
|
|||||||
assert entered == ["enter", "exit"]
|
assert entered == ["enter", "exit"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_eager_cfm_reads_the_step_sizes_to_the_host_once(monkeypatch):
|
||||||
|
"""The eager Euler loop must not sync the stream for ``dt`` on every decode."""
|
||||||
|
adapter = BatchedToken2Wav(_FakeToken2Wav())
|
||||||
|
reads: list[str] = []
|
||||||
|
|
||||||
|
def recording(name):
|
||||||
|
original = getattr(torch.Tensor, name)
|
||||||
|
|
||||||
|
def read(self, *args, **kwargs):
|
||||||
|
# The fake estimator records its inputs with ``tolist``; skip those.
|
||||||
|
if sys._getframe(1).f_code.co_filename != __file__:
|
||||||
|
reads.append(name)
|
||||||
|
return original(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return read
|
||||||
|
|
||||||
|
for name in ("item", "tolist"):
|
||||||
|
monkeypatch.setattr(torch.Tensor, name, recording(name))
|
||||||
|
|
||||||
|
def decode():
|
||||||
|
adapter._decode_cfm(
|
||||||
|
torch.ones((1, 1, 2)),
|
||||||
|
torch.ones((1, 1)),
|
||||||
|
torch.zeros((1, 1, 2)),
|
||||||
|
cnn_cache=None,
|
||||||
|
att_cache=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
decode()
|
||||||
|
assert reads == ["tolist"]
|
||||||
|
decode()
|
||||||
|
assert reads == ["tolist"]
|
||||||
|
|
||||||
|
|
||||||
def test_ragged_decode_bypasses_exact_shape_accelerators():
|
def test_ragged_decode_bypasses_exact_shape_accelerators():
|
||||||
adapter = BatchedToken2Wav(_FakeToken2Wav())
|
adapter = BatchedToken2Wav(_FakeToken2Wav())
|
||||||
_enable_fake_ragged_kernel(adapter)
|
_enable_fake_ragged_kernel(adapter)
|
||||||
@@ -963,6 +1039,44 @@ def test_estimator_cache_stack_split_round_trip_preserves_cfg_rows():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("attention_dtype", [torch.float32, torch.bfloat16])
|
||||||
|
def test_split_flow_cache_trims_each_request_like_the_batch_trim(attention_dtype):
|
||||||
|
"""Trimming per request while splitting equals trimming the stacked batch first."""
|
||||||
|
adapter = BatchedToken2Wav(_FakeToken2Wav(), bfloat16_attention_cache=attention_dtype == torch.bfloat16)
|
||||||
|
batch_size, prompt_len, frames = 3, 5, 12
|
||||||
|
torch.manual_seed(0)
|
||||||
|
stacked = {
|
||||||
|
"conformer_cnn_cache": torch.randn(batch_size, 2, 3),
|
||||||
|
"conformer_att_cache": torch.randn(2, batch_size, 2, 4, 6),
|
||||||
|
"estimator_cnn_cache": torch.randn(4, 2, 2 * batch_size, 3, 2),
|
||||||
|
"estimator_att_cache": torch.randn(4, 2, 2 * batch_size, 2, frames, 6),
|
||||||
|
}
|
||||||
|
|
||||||
|
restored = adapter._split_flow_cache(stacked, batch_size, estimator_att_keep=(prompt_len, 4))
|
||||||
|
|
||||||
|
att = stacked["estimator_att_cache"]
|
||||||
|
trimmed = torch.cat((att[..., :prompt_len, :], att[..., -4:, :]), dim=4)
|
||||||
|
for row, request in enumerate(restored):
|
||||||
|
expected = torch.cat(
|
||||||
|
(trimmed[:, :, row : row + 1], trimmed[:, :, batch_size + row : batch_size + row + 1]),
|
||||||
|
dim=2,
|
||||||
|
).to(attention_dtype)
|
||||||
|
assert request["estimator_att_cache"].dtype == attention_dtype
|
||||||
|
torch.testing.assert_close(request["estimator_att_cache"], expected)
|
||||||
|
torch.testing.assert_close(
|
||||||
|
request["estimator_cnn_cache"],
|
||||||
|
torch.cat(
|
||||||
|
(
|
||||||
|
stacked["estimator_cnn_cache"][:, :, row : row + 1],
|
||||||
|
stacked["estimator_cnn_cache"][:, :, batch_size + row : batch_size + row + 1],
|
||||||
|
),
|
||||||
|
dim=2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
untrimmed = adapter._split_flow_cache(stacked, batch_size, estimator_att_keep=(frames, 4))
|
||||||
|
assert untrimmed[0]["estimator_att_cache"].shape[4] == frames
|
||||||
|
|
||||||
|
|
||||||
def test_bfloat16_estimator_attention_cache_materializes_only_current_timestep():
|
def test_bfloat16_estimator_attention_cache_materializes_only_current_timestep():
|
||||||
token2wav = _FakeToken2Wav()
|
token2wav = _FakeToken2Wav()
|
||||||
adapter = BatchedToken2Wav(token2wav, bfloat16_attention_cache=True)
|
adapter = BatchedToken2Wav(token2wav, bfloat16_attention_cache=True)
|
||||||
@@ -970,9 +1084,8 @@ def test_bfloat16_estimator_attention_cache_materializes_only_current_timestep()
|
|||||||
states = adapter.setup_batch(prompt, 2)
|
states = adapter.setup_batch(prompt, 2)
|
||||||
|
|
||||||
assert all(state.flow_cache["estimator_att_cache"].dtype == torch.bfloat16 for state in states)
|
assert all(state.flow_cache["estimator_att_cache"].dtype == torch.bfloat16 for state in states)
|
||||||
assert (
|
# Both requests start from the one shared prompt state.
|
||||||
states[0].flow_cache["estimator_att_cache"].data_ptr() != states[1].flow_cache["estimator_att_cache"].data_ptr()
|
assert states[0] is states[1]
|
||||||
)
|
|
||||||
|
|
||||||
stacked = adapter._stack_flow_cache(states)
|
stacked = adapter._stack_flow_cache(states)
|
||||||
assert stacked["estimator_att_cache"].dtype == torch.bfloat16
|
assert stacked["estimator_att_cache"].dtype == torch.bfloat16
|
||||||
@@ -991,6 +1104,10 @@ def test_bfloat16_estimator_attention_cache_materializes_only_current_timestep()
|
|||||||
last_chunk=False,
|
last_chunk=False,
|
||||||
)
|
)
|
||||||
assert all(state.flow_cache["estimator_att_cache"].dtype == torch.bfloat16 for state in next_states)
|
assert all(state.flow_cache["estimator_att_cache"].dtype == torch.bfloat16 for state in next_states)
|
||||||
|
assert (
|
||||||
|
next_states[0].flow_cache["estimator_att_cache"].data_ptr()
|
||||||
|
!= next_states[1].flow_cache["estimator_att_cache"].data_ptr()
|
||||||
|
)
|
||||||
assert token2wav.flow.decoder.estimator.attention_cache_dtypes[-2:] == [torch.float32, torch.float32]
|
assert token2wav.flow.decoder.estimator.attention_cache_dtypes[-2:] == [torch.float32, torch.float32]
|
||||||
|
|
||||||
|
|
||||||
@@ -1117,9 +1234,17 @@ def test_initial_empty_segment_marker_initializes_stream_without_audio():
|
|||||||
assert "duplex" in model._states
|
assert "duplex" in model._states
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("setup_cache_size,expected_calls", [(0, [2, 2, 1]), (1, [2, 1])])
|
@pytest.mark.parametrize("setup_cache_size,expected_calls", [(0, [1, 1, 1]), (1, [1])])
|
||||||
def test_initial_empty_segment_markers_respect_initial_batch_limit(setup_cache_size, expected_calls):
|
def test_initial_empty_segment_markers_respect_initial_batch_limit(setup_cache_size, expected_calls):
|
||||||
model, token2wav = _model(initial=2, setup_cache_size=setup_cache_size)
|
model, token2wav = _model(initial=2, setup_cache_size=setup_cache_size)
|
||||||
|
setup_batches: list[int] = []
|
||||||
|
setup_batch = model.backend.setup_batch
|
||||||
|
|
||||||
|
def record_setup(features, batch_size):
|
||||||
|
setup_batches.append(batch_size)
|
||||||
|
return setup_batch(features, batch_size)
|
||||||
|
|
||||||
|
model.backend.setup_batch = record_setup
|
||||||
names = ["a", "b", "c", "d", "e"]
|
names = ["a", "b", "c", "d", "e"]
|
||||||
boundaries = []
|
boundaries = []
|
||||||
for name in names:
|
for name in names:
|
||||||
@@ -1135,6 +1260,8 @@ def test_initial_empty_segment_markers_respect_initial_batch_limit(setup_cache_s
|
|||||||
|
|
||||||
output = _forward(model, boundaries)
|
output = _forward(model, boundaries)
|
||||||
|
|
||||||
|
assert setup_batches == [2, 2, 1]
|
||||||
|
# One shared prompt solve per setup, or one in all with the setup cache.
|
||||||
assert token2wav.flow.encoder.calls == expected_calls
|
assert token2wav.flow.encoder.calls == expected_calls
|
||||||
assert [audio.numel() for audio in output.multimodal_outputs["model_outputs"]] == [0] * len(names)
|
assert [audio.numel() for audio in output.multimodal_outputs["model_outputs"]] == [0] * len(names)
|
||||||
assert set(model._states) == set(names)
|
assert set(model._states) == set(names)
|
||||||
@@ -1631,8 +1758,8 @@ def test_setup_cache_misses_when_cfm_graph_padding_is_disabled(monkeypatch, disa
|
|||||||
assert unpadded[0] is not padded[0]
|
assert unpadded[0] is not padded[0]
|
||||||
assert reused[0] is unpadded[0]
|
assert reused[0] is unpadded[0]
|
||||||
assert adapter.flow.encoder.calls == [1, 1]
|
assert adapter.flow.encoder.calls == [1, 1]
|
||||||
assert padded_keys == [(prompt.cache_key, 1, 16)]
|
assert padded_keys == [(prompt.cache_key, 16)]
|
||||||
assert list(adapter._setup_cache) == [(prompt.cache_key, 1, 0)]
|
assert list(adapter._setup_cache) == [(prompt.cache_key, 0)]
|
||||||
|
|
||||||
|
|
||||||
def test_padded_chunk_keeps_the_cross_chunk_caches_on_the_valid_boundary():
|
def test_padded_chunk_keeps_the_cross_chunk_caches_on_the_valid_boundary():
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -170,7 +170,7 @@ class TestDeployTopology:
|
|||||||
assert stages[1].yaml_engine_args["custom_process_next_stage_input_func"].endswith(expected_processor)
|
assert stages[1].yaml_engine_args["custom_process_next_stage_input_func"].endswith(expected_processor)
|
||||||
assert "hf_overrides" not in stages[1].yaml_engine_args
|
assert "hf_overrides" not in stages[1].yaml_engine_args
|
||||||
if filename == "minicpmo_4_5.yaml":
|
if filename == "minicpmo_4_5.yaml":
|
||||||
assert [stage.yaml_engine_args["max_num_seqs"] for stage in stages] == [4, 4, 4]
|
assert [stage.yaml_engine_args["max_num_seqs"] for stage in stages] == [16, 16, 16]
|
||||||
memory_utilizations = [stage.yaml_engine_args["gpu_memory_utilization"] for stage in stages]
|
memory_utilizations = [stage.yaml_engine_args["gpu_memory_utilization"] for stage in stages]
|
||||||
assert memory_utilizations == [
|
assert memory_utilizations == [
|
||||||
0.55,
|
0.55,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ session_mode: duplex
|
|||||||
# A smaller window serializes first-packet generation for concurrent chat turns.
|
# A smaller window serializes first-packet generation for concurrent chat turns.
|
||||||
duplex_session:
|
duplex_session:
|
||||||
# Matches Stage 0/1 max_num_seqs below.
|
# Matches Stage 0/1 max_num_seqs below.
|
||||||
max_sessions: 4
|
max_sessions: 16
|
||||||
|
|
||||||
trust_remote_code: true
|
trust_remote_code: true
|
||||||
enable_prefix_caching: false
|
enable_prefix_caching: false
|
||||||
@@ -27,12 +27,17 @@ connectors:
|
|||||||
# HiFTGraphWrapper.derive_capture_bucket_size explicitly includes it.
|
# HiFTGraphWrapper.derive_capture_bucket_size explicitly includes it.
|
||||||
enable_hift_graph: true
|
enable_hift_graph: true
|
||||||
enable_cfm_graph: true
|
enable_cfm_graph: true
|
||||||
|
# Whole-Euler CUDA graphs exist for batch 1, 2, 4, ... up to
|
||||||
|
# micro_batch_size, which defaults to Stage 2 max_num_seqs: all graphs
|
||||||
|
# share one attention-cache arena of that many requests (~0.6 GiB each
|
||||||
|
# in fp32, 10 GiB at 16). Larger batches replay micro-batches;
|
||||||
|
# B > max_graph_batch runs eager.
|
||||||
|
max_graph_batch: 16
|
||||||
cfm_max_graphs: 32
|
cfm_max_graphs: 32
|
||||||
cfm_graph_bucket_frames: 16
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- stage_id: 0
|
- stage_id: 0
|
||||||
max_num_seqs: 4
|
max_num_seqs: 16
|
||||||
# Leave headroom on the shared GPU for Talker KV + concurrent Code2Wav.
|
# Leave headroom on the shared GPU for Talker KV + concurrent Code2Wav.
|
||||||
gpu_memory_utilization: 0.55
|
gpu_memory_utilization: 0.55
|
||||||
enforce_eager: false
|
enforce_eager: false
|
||||||
@@ -62,7 +67,7 @@ stages:
|
|||||||
repetition_penalty: 1.0
|
repetition_penalty: 1.0
|
||||||
|
|
||||||
- stage_id: 1
|
- stage_id: 1
|
||||||
max_num_seqs: 4
|
max_num_seqs: 16
|
||||||
# Preserve the checkpoint's full-attention Talker prompt while it fits;
|
# Preserve the checkpoint's full-attention Talker prompt while it fits;
|
||||||
# native duplex rolls over to a one-chunk window at context capacity.
|
# native duplex rolls over to a one-chunk window at context capacity.
|
||||||
# Leave device-level headroom for the colocated HiFi-GAN cuDNN workspace.
|
# Leave device-level headroom for the colocated HiFi-GAN cuDNN workspace.
|
||||||
@@ -92,7 +97,7 @@ stages:
|
|||||||
detokenize: false
|
detokenize: false
|
||||||
|
|
||||||
- stage_id: 2
|
- stage_id: 2
|
||||||
max_num_seqs: 4
|
max_num_seqs: 16
|
||||||
gpu_memory_utilization: 0.18
|
gpu_memory_utilization: 0.18
|
||||||
enforce_eager: true
|
enforce_eager: true
|
||||||
enable_chunked_prefill: false
|
enable_chunked_prefill: false
|
||||||
@@ -113,10 +118,11 @@ platforms:
|
|||||||
stages:
|
stages:
|
||||||
- stage_id: 1
|
- stage_id: 1
|
||||||
# Full-attention occupancy is bounded by max_num_seqs * max_model_len
|
# Full-attention occupancy is bounded by max_num_seqs * max_model_len
|
||||||
# (4 * 4096 = 16384 tokens). Keep the reported CUDA KV token capacity
|
# (16 * 4096 = 65536 tokens). Keep the reported CUDA KV token capacity
|
||||||
# above that bound because physical-preemption replay is unsupported.
|
# above that bound because physical-preemption replay is unsupported
|
||||||
# The explicit cap leaves device headroom for concurrent Stage 2.
|
# (2 GiB holds ~35k tokens). The explicit cap leaves device headroom
|
||||||
kv_cache_memory_bytes: 2147483648 # 2 GiB
|
# for concurrent Stage 2.
|
||||||
|
kv_cache_memory_bytes: 4294967296 # 4 GiB
|
||||||
|
|
||||||
npu:
|
npu:
|
||||||
stages:
|
stages:
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ connectors:
|
|||||||
enable_hift_graph: true
|
enable_hift_graph: true
|
||||||
enable_cfm_graph: true
|
enable_cfm_graph: true
|
||||||
cfm_max_graphs: 32
|
cfm_max_graphs: 32
|
||||||
cfm_graph_bucket_frames: 16
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- stage_id: 0
|
- stage_id: 0
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ connectors:
|
|||||||
enable_hift_graph: true
|
enable_hift_graph: true
|
||||||
enable_cfm_graph: true
|
enable_cfm_graph: true
|
||||||
cfm_max_graphs: 32
|
cfm_max_graphs: 32
|
||||||
cfm_graph_bucket_frames: 16
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- stage_id: 0
|
- stage_id: 0
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ connectors:
|
|||||||
enable_hift_graph: true
|
enable_hift_graph: true
|
||||||
enable_cfm_graph: true
|
enable_cfm_graph: true
|
||||||
cfm_max_graphs: 32
|
cfm_max_graphs: 32
|
||||||
cfm_graph_bucket_frames: 16
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- stage_id: 0
|
- stage_id: 0
|
||||||
|
|||||||
@@ -15,7 +15,16 @@ import torch.nn as nn
|
|||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
from vllm.logger import init_logger
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
from .cuda_graph_wrapper import CFMGraphWrapper, HiFTGraphWrapper
|
from .cuda_graph_wrapper import (
|
||||||
|
CFMGraphWrapper,
|
||||||
|
HiFTGraphWrapper,
|
||||||
|
WholeEulerCFMGraphWrapper,
|
||||||
|
_att_keep_ranges,
|
||||||
|
_copy_frame_segments,
|
||||||
|
_euler_step,
|
||||||
|
_euler_timeline,
|
||||||
|
_zero_padded_cnn_cache,
|
||||||
|
)
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
@@ -25,6 +34,17 @@ _SILENCE_TOKEN = 4218
|
|||||||
# than this many codec tokens overflows the PE slice (matrix_ac vs matrix_bd).
|
# than this many codec tokens overflows the PE slice (matrix_ac vs matrix_bd).
|
||||||
_DEFAULT_RELPOS_MAX_POS = 5000
|
_DEFAULT_RELPOS_MAX_POS = 5000
|
||||||
_MAX_ENCODE_TOKEN_CAP = 1024
|
_MAX_ENCODE_TOKEN_CAP = 1024
|
||||||
|
# Streaming cache trim (stepaudio2 ``Token2wav.stream``): past ``prompt_len +
|
||||||
|
# 100`` frames, keep the first ``prompt_len`` frames and the last 100.
|
||||||
|
_CACHE_TRIM_SUFFIX = 100
|
||||||
|
|
||||||
|
|
||||||
|
def _trim_streaming_cache(cache: torch.Tensor, prompt_len: int) -> torch.Tensor:
|
||||||
|
"""The streaming trim of a cache whose frame axis is ``-2``."""
|
||||||
|
ranges = _att_keep_ranges(int(cache.shape[-2]), (prompt_len, _CACHE_TRIM_SUFFIX))
|
||||||
|
if len(ranges) == 1:
|
||||||
|
return cache
|
||||||
|
return torch.cat([cache[..., start : start + length, :] for start, length in ranges], dim=-2)
|
||||||
|
|
||||||
|
|
||||||
def relpos_encode_token_budget(
|
def relpos_encode_token_budget(
|
||||||
@@ -47,6 +67,26 @@ def relpos_encode_token_budget(
|
|||||||
return max(lookahead + 1, min(int(cap), room))
|
return max(lookahead + 1, min(int(cap), room))
|
||||||
|
|
||||||
|
|
||||||
|
def _supports_ragged_kernel(estimator: nn.Module) -> bool:
|
||||||
|
"""Whether ``_blocks_forward_chunk_ragged`` can run this estimator's DiT blocks."""
|
||||||
|
blocks = getattr(estimator, "blocks", None)
|
||||||
|
if not blocks or not hasattr(estimator, "in_proj") or not hasattr(estimator, "final_layer"):
|
||||||
|
return False
|
||||||
|
block = blocks[0]
|
||||||
|
attn = getattr(block, "attn", None)
|
||||||
|
conv_blocks = getattr(getattr(block, "conv", None), "block", None)
|
||||||
|
return (
|
||||||
|
hasattr(block, "adaLN_modulation")
|
||||||
|
and all(
|
||||||
|
hasattr(attn, name)
|
||||||
|
for name in ("forward_chunk", "to_q", "to_k", "to_v", "to_heads", "q_norm", "k_norm", "proj")
|
||||||
|
)
|
||||||
|
and conv_blocks is not None
|
||||||
|
and len(conv_blocks) >= 8
|
||||||
|
and all(isinstance(conv_blocks[index], nn.Conv1d) for index in (1, 6))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _cfm_pad_frames(
|
def _cfm_pad_frames(
|
||||||
*,
|
*,
|
||||||
mel_frames: int,
|
mel_frames: int,
|
||||||
@@ -98,38 +138,6 @@ def _zero_padded_frames(tensor: torch.Tensor, valid_frames: int | None) -> None:
|
|||||||
tensor[..., valid_frames:] = 0.0
|
tensor[..., valid_frames:] = 0.0
|
||||||
|
|
||||||
|
|
||||||
def _zero_padded_cnn_cache(
|
|
||||||
cnn_cache: torch.Tensor,
|
|
||||||
estimator: nn.Module,
|
|
||||||
pad_frames: int,
|
|
||||||
) -> None:
|
|
||||||
"""Clear the cache positions that come from padded frames, in place.
|
|
||||||
|
|
||||||
Each block's CNN cache holds the tail of its convolution output
|
|
||||||
(``new_cnn_cache = x[..., -causal_padding[0]:]``, inside ``stepaudio2``),
|
|
||||||
so when the padding sits at the chunk tail those positions are
|
|
||||||
padding-derived and would otherwise become the next chunk's left context.
|
|
||||||
Padding is at most ``pad_frames`` wide, so only the trailing positions that
|
|
||||||
can come from it are cleared; the valid part of the window is kept.
|
|
||||||
"""
|
|
||||||
blocks = estimator.blocks
|
|
||||||
if pad_frames > 0 and blocks and len(blocks) == cnn_cache.shape[0]:
|
|
||||||
width = int(cnn_cache.shape[-1])
|
|
||||||
if width > 0 and all(int(block.conv.block[1].causal_padding[0]) == width for block in blocks):
|
|
||||||
# _estimator_buffers packs equal-width blocks into one tensor.
|
|
||||||
# Clear their shared tail with one write instead of one per block.
|
|
||||||
cnn_cache[..., max(0, width - pad_frames) :] = 0.0
|
|
||||||
return
|
|
||||||
|
|
||||||
for index, block in enumerate(estimator.blocks):
|
|
||||||
width = int(block.conv.block[1].causal_padding[0])
|
|
||||||
if width <= 0:
|
|
||||||
continue
|
|
||||||
zero_from = max(0, width - pad_frames)
|
|
||||||
if zero_from < width:
|
|
||||||
cnn_cache[index][..., zero_from:] = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def plan_token2wav_encode_slices(
|
def plan_token2wav_encode_slices(
|
||||||
num_frames: int,
|
num_frames: int,
|
||||||
*,
|
*,
|
||||||
@@ -297,6 +305,7 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
token2wav.speech_window.detach().clone(),
|
token2wav.speech_window.detach().clone(),
|
||||||
persistent=False,
|
persistent=False,
|
||||||
)
|
)
|
||||||
|
self._timeline_cache: dict[tuple[torch.device, torch.dtype], tuple[torch.Tensor, list[float]]] = {}
|
||||||
self.hift_graph_wrapper: HiFTGraphWrapper | None = None
|
self.hift_graph_wrapper: HiFTGraphWrapper | None = None
|
||||||
graph_config = dict(hift_graph_config or {})
|
graph_config = dict(hift_graph_config or {})
|
||||||
if bool(graph_config.get("enabled", False)):
|
if bool(graph_config.get("enabled", False)):
|
||||||
@@ -312,26 +321,79 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
"MiniCPM-o HiFT CUDA Graph requires source_cache_len to be divisible by mel_cache_len"
|
"MiniCPM-o HiFT CUDA Graph requires source_cache_len to be divisible by mel_cache_len"
|
||||||
)
|
)
|
||||||
capture_batch_sizes = graph_config.get("capture_batch_sizes", [1])
|
capture_batch_sizes = graph_config.get("capture_batch_sizes", [1])
|
||||||
logger.info("Enabling HiFT CUDA Graph with batch sizes %s", capture_batch_sizes)
|
max_serial_batch = int(graph_config.get("max_serial_batch", 4))
|
||||||
|
logger.info(
|
||||||
|
"Enabling HiFT CUDA Graph with batch sizes %s (max_serial_batch=%d)",
|
||||||
|
capture_batch_sizes,
|
||||||
|
max_serial_batch,
|
||||||
|
)
|
||||||
self.hift_graph_wrapper = HiFTGraphWrapper(
|
self.hift_graph_wrapper = HiFTGraphWrapper(
|
||||||
token2wav=token2wav,
|
token2wav=token2wav,
|
||||||
connector_config=dict(connector_config),
|
connector_config=dict(connector_config),
|
||||||
capture_batch_sizes=capture_batch_sizes,
|
capture_batch_sizes=capture_batch_sizes,
|
||||||
|
max_serial_batch=max_serial_batch,
|
||||||
)
|
)
|
||||||
with torch.inference_mode(), _autocast_disabled(hift_parameter.device):
|
with torch.inference_mode(), _autocast_disabled(hift_parameter.device):
|
||||||
self.hift_graph_wrapper.capture()
|
self.hift_graph_wrapper.capture()
|
||||||
logger.info("HiFT CUDA Graph captured successfully")
|
logger.info("HiFT CUDA Graph captured successfully")
|
||||||
self._cfm_graph_wrapper: CFMGraphWrapper | None = None
|
self._cfm_graph_wrapper: CFMGraphWrapper | None = None
|
||||||
|
self._whole_euler_graph_wrapper: WholeEulerCFMGraphWrapper | None = None
|
||||||
cfm_graph_cfg = dict(cfm_graph_config or {})
|
cfm_graph_cfg = dict(cfm_graph_config or {})
|
||||||
if bool(cfm_graph_cfg.get("enabled", False)):
|
if bool(cfm_graph_cfg.get("enabled", False)):
|
||||||
flow_parameter = next(self.flow.parameters(), None)
|
flow_parameter = next(self.flow.parameters(), None)
|
||||||
if flow_parameter is not None and flow_parameter.device.type == "cuda":
|
if flow_parameter is not None and flow_parameter.device.type == "cuda":
|
||||||
estimator = self.flow.decoder.estimator
|
estimator = self.flow.decoder.estimator
|
||||||
|
max_graphs = int(cfm_graph_cfg.get("max_graphs", 32))
|
||||||
|
max_serial_batch = int(cfm_graph_cfg.get("max_serial_batch", 4))
|
||||||
|
max_graph_batch_cfg = cfm_graph_cfg.get("max_graph_batch")
|
||||||
|
max_graph_batch = int(max_graph_batch_cfg) if max_graph_batch_cfg is not None else None
|
||||||
|
micro_batch_size_cfg = cfm_graph_cfg.get("micro_batch_size")
|
||||||
|
micro_batch_size = int(micro_batch_size_cfg) if micro_batch_size_cfg is not None else 4
|
||||||
|
if bool(cfm_graph_cfg.get("enable_whole_euler", True)) and self._trt_stepper is None:
|
||||||
|
capture_frames = cfm_graph_cfg.get("capture_frames")
|
||||||
|
if capture_frames is None and connector_config is not None:
|
||||||
|
# The steady chunk's mel width, derived like HiFTGraphWrapper's
|
||||||
|
# capture bucket: steady chunks run unpadded and shorter
|
||||||
|
# chunks share their graph under a mask.
|
||||||
|
capture_frames = int(connector_config["codec_chunk_frames"]) * int(
|
||||||
|
getattr(self.flow, "token_mel_ratio", 2)
|
||||||
|
)
|
||||||
|
query_bucket_frames = int(capture_frames if capture_frames is not None else 64)
|
||||||
|
self._whole_euler_graph_wrapper = WholeEulerCFMGraphWrapper(
|
||||||
|
estimator=estimator,
|
||||||
|
n_timesteps=self.n_timesteps,
|
||||||
|
inference_cfg_rate=getattr(self.flow.decoder, "inference_cfg_rate", 0.7),
|
||||||
|
att_cache_dtype=self._estimator_att_cache_dtype,
|
||||||
|
max_graphs=max_graphs,
|
||||||
|
max_serial_batch=max_serial_batch,
|
||||||
|
max_graph_batch=max_graph_batch,
|
||||||
|
micro_batch_size=micro_batch_size,
|
||||||
|
query_bucket_frames=query_bucket_frames,
|
||||||
|
pad_max_rows=cfm_graph_cfg.get("pad_max_rows"),
|
||||||
|
# Looked up at capture time, so a replaced ragged kernel is the one captured.
|
||||||
|
ragged_body=(
|
||||||
|
(lambda *args: self._blocks_forward_chunk_ragged(*args))
|
||||||
|
if _supports_ragged_kernel(estimator)
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Whole-Euler CFM CUDA Graph enabled "
|
||||||
|
"(max_graphs=%d, max_serial_batch=%d, max_graph_batch=%s, "
|
||||||
|
"micro_batch_size=%d, query_bucket_frames=%d)",
|
||||||
|
max_graphs,
|
||||||
|
max_serial_batch,
|
||||||
|
str(max_graph_batch),
|
||||||
|
micro_batch_size,
|
||||||
|
query_bucket_frames,
|
||||||
|
)
|
||||||
|
elif self._trt_stepper is not None and bool(cfm_graph_cfg.get("enable_whole_euler", True)):
|
||||||
|
logger.info("Whole-Euler CFM CUDA Graph disabled because TensorRT stepper is configured")
|
||||||
self._cfm_graph_wrapper = CFMGraphWrapper(
|
self._cfm_graph_wrapper = CFMGraphWrapper(
|
||||||
graph_fn=estimator.blocks_forward_chunk,
|
graph_fn=estimator.blocks_forward_chunk,
|
||||||
max_graphs=int(cfm_graph_cfg.get("max_graphs", 32)),
|
max_graphs=max_graphs,
|
||||||
)
|
)
|
||||||
logger.info("CFM CUDA Graph enabled (max_graphs=%d)", int(cfm_graph_cfg.get("max_graphs", 32)))
|
logger.info("CFM CUDA Graph enabled (max_graphs=%d)", max_graphs)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"CFM CUDA Graph is disabled on device type %s",
|
"CFM CUDA Graph is disabled on device type %s",
|
||||||
@@ -341,7 +403,9 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
# chunk up to a multiple of this many frames so the graph cache key
|
# chunk up to a multiple of this many frames so the graph cache key
|
||||||
# space stays small (0 disables bucketing, e.g. when graphs are off).
|
# space stays small (0 disables bucketing, e.g. when graphs are off).
|
||||||
self._cfm_graph_bucket_frames = (
|
self._cfm_graph_bucket_frames = (
|
||||||
int(cfm_graph_cfg.get("bucket_frames", 0)) if self._cfm_graph_wrapper is not None else 0
|
int(cfm_graph_cfg.get("bucket_frames", 0))
|
||||||
|
if (self._cfm_graph_wrapper is not None or self._whole_euler_graph_wrapper is not None)
|
||||||
|
else 0
|
||||||
)
|
)
|
||||||
if self._cfm_graph_bucket_frames > 1:
|
if self._cfm_graph_bucket_frames > 1:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -350,9 +414,7 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
)
|
)
|
||||||
self._prompt_features: dict[tuple[str, str], PromptFeatures] = {}
|
self._prompt_features: dict[tuple[str, str], PromptFeatures] = {}
|
||||||
self._setup_cache_size = setup_cache_size
|
self._setup_cache_size = setup_cache_size
|
||||||
self._setup_cache: OrderedDict[tuple[tuple[str, str], int, int], tuple[BatchedToken2WavState, ...]] = (
|
self._setup_cache: OrderedDict[tuple[tuple[str, str], int], BatchedToken2WavState] = OrderedDict()
|
||||||
OrderedDict()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _hift_inference(
|
def _hift_inference(
|
||||||
self,
|
self,
|
||||||
@@ -507,6 +569,7 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
attn_mask: torch.Tensor | None = None,
|
attn_mask: torch.Tensor | None = None,
|
||||||
valid_lengths: list[int] | None = None,
|
valid_lengths: list[int] | None = None,
|
||||||
valid_frames: int | None = None,
|
valid_frames: int | None = None,
|
||||||
|
time_embedding: torch.Tensor | None = None,
|
||||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
if self._trt_stepper is not None and valid_lengths is None:
|
if self._trt_stepper is not None and valid_lengths is None:
|
||||||
out, new_cnn, new_att = self._trt_stepper.step(
|
out, new_cnn, new_att = self._trt_stepper.step(
|
||||||
@@ -519,7 +582,8 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
att_cache=att_cache,
|
att_cache=att_cache,
|
||||||
)
|
)
|
||||||
return out.to(mu.dtype), new_cnn, new_att
|
return out.to(mu.dtype), new_cnn, new_att
|
||||||
time_embedding = estimator.t_embedder(time).unsqueeze(1)
|
if time_embedding is None:
|
||||||
|
time_embedding = estimator.t_embedder(time).unsqueeze(1)
|
||||||
width = int(x.shape[-1])
|
width = int(x.shape[-1])
|
||||||
speaker_features = speakers.unsqueeze(-1).expand(-1, -1, width)
|
speaker_features = speakers.unsqueeze(-1).expand(-1, -1, width)
|
||||||
if valid_frames is not None and valid_frames < width:
|
if valid_frames is not None and valid_frames < width:
|
||||||
@@ -583,14 +647,95 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
valid_lengths: torch.Tensor,
|
valid_lengths: torch.Tensor,
|
||||||
width: int,
|
width: int,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
|
"""The ``width`` frames of ``history`` (b, width + t, c) ending each row's valid frames, as (b, c, width)."""
|
||||||
indices = valid_lengths[:, None] + torch.arange(width, device=history.device)[None, :]
|
indices = valid_lengths[:, None] + torch.arange(width, device=history.device)[None, :]
|
||||||
return history.gather(
|
return history.gather(1, indices[:, :, None].expand(-1, -1, int(history.shape[2]))).transpose(1, 2)
|
||||||
2,
|
|
||||||
indices[:, None, :].expand(-1, int(history.shape[1]), -1),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _causal_conv_frames_major(conv: nn.Conv1d, history: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""``conv`` over ``history`` (b, width + t, c_in) as (b, t, c_out), without a layout change.
|
||||||
|
|
||||||
|
The DiT keeps activations as (b, t, c), which already is NHWC for a
|
||||||
|
height-1 image. Upstream transposes them to (b, c, t) for ``Conv1d``,
|
||||||
|
and cuDNN converts that to NHWC and back around every convolution. As
|
||||||
|
a channels-last conv2d it reads the activations where they are. The
|
||||||
|
channels-last weight is converted once and reused. Other devices run
|
||||||
|
upstream's ``Conv1d`` layout (Ascend has no channels-last format).
|
||||||
|
"""
|
||||||
|
if not history.is_cuda:
|
||||||
|
output = F.conv1d(
|
||||||
|
history.transpose(1, 2),
|
||||||
|
conv.weight,
|
||||||
|
conv.bias,
|
||||||
|
conv.stride,
|
||||||
|
conv.padding,
|
||||||
|
conv.dilation,
|
||||||
|
conv.groups,
|
||||||
|
)
|
||||||
|
return output.transpose(1, 2)
|
||||||
|
weight = conv.weight
|
||||||
|
version = (weight.data_ptr(), weight._version, weight.dtype)
|
||||||
|
cached = conv.__dict__.get("_channels_last_weight")
|
||||||
|
if cached is None or cached[0] != version:
|
||||||
|
cached = (version, weight.detach().unsqueeze(2).contiguous(memory_format=torch.channels_last))
|
||||||
|
conv.__dict__["_channels_last_weight"] = cached
|
||||||
|
output = F.conv2d(
|
||||||
|
history.transpose(1, 2).unsqueeze(2),
|
||||||
|
cached[1],
|
||||||
|
conv.bias,
|
||||||
|
(1, conv.stride[0]),
|
||||||
|
(0, conv.padding[0]),
|
||||||
|
(1, conv.dilation[0]),
|
||||||
|
conv.groups,
|
||||||
|
)
|
||||||
|
return output.squeeze(2).transpose(1, 2)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _attend_in_place(
|
||||||
|
attn: nn.Module,
|
||||||
|
x: torch.Tensor,
|
||||||
|
att_cache: torch.Tensor | None,
|
||||||
|
attn_mask: torch.Tensor | None,
|
||||||
|
kv: torch.Tensor,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""``attn.forward_chunk`` that keeps the new keys and values in ``kv``.
|
||||||
|
|
||||||
|
``kv`` (b, heads, >= t + cache frames, 2 * head_dim) is the block's new
|
||||||
|
cache, ``[current | cache]`` like upstream's. Upstream concatenates the
|
||||||
|
whole cache behind the current keys and values, then both into the new
|
||||||
|
cache, which the caller copied into ``kv``: four passes over Stage 2's
|
||||||
|
largest tensor. Here only the current chunk is written and attention reads ``kv`` in
|
||||||
|
place. A cache that already sits behind the current chunk in ``kv``
|
||||||
|
(Whole-Euler) is not moved; any other is copied there once. Other
|
||||||
|
devices keep upstream's attention and copy its cache into ``kv``.
|
||||||
|
"""
|
||||||
|
if kv.dtype != x.dtype or not x.is_cuda:
|
||||||
|
# A reduced-precision cache attends in the activation dtype.
|
||||||
|
x_att, new_att = attn.forward_chunk(x, att_cache, attn_mask)
|
||||||
|
kv[:, :, : int(new_att.shape[2])].copy_(new_att)
|
||||||
|
return x_att
|
||||||
|
batch, frames, _ = x.shape
|
||||||
|
q = attn.q_norm(attn.to_heads(attn.to_q(x)))
|
||||||
|
k = attn.k_norm(attn.to_heads(attn.to_k(x)))
|
||||||
|
v = attn.to_heads(attn.to_v(x))
|
||||||
|
cached = 0 if att_cache is None else int(att_cache.shape[2])
|
||||||
|
kv = kv[:, :, : frames + cached]
|
||||||
|
behind = kv[:, :, frames:]
|
||||||
|
if cached and (att_cache.data_ptr() != behind.data_ptr() or att_cache.stride() != behind.stride()):
|
||||||
|
behind.copy_(att_cache)
|
||||||
|
head_dim = int(k.shape[3])
|
||||||
|
kv[..., :frames, :head_dim].copy_(k)
|
||||||
|
kv[..., :frames, head_dim:].copy_(v)
|
||||||
|
out = F.scaled_dot_product_attention(
|
||||||
|
q,
|
||||||
|
kv[..., :head_dim],
|
||||||
|
kv[..., head_dim:],
|
||||||
|
attn_mask=None if attn_mask is None else attn_mask.unsqueeze(1),
|
||||||
|
)
|
||||||
|
return attn.proj(out.transpose(1, 2).reshape(batch, frames, -1))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _blocks_forward_chunk_ragged(
|
def _blocks_forward_chunk_ragged(
|
||||||
self,
|
|
||||||
estimator: nn.Module,
|
estimator: nn.Module,
|
||||||
estimator_input: torch.Tensor,
|
estimator_input: torch.Tensor,
|
||||||
time_embedding: torch.Tensor,
|
time_embedding: torch.Tensor,
|
||||||
@@ -599,14 +744,24 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
att_cache: Any,
|
att_cache: Any,
|
||||||
cnn_cache_buffer: torch.Tensor,
|
cnn_cache_buffer: torch.Tensor,
|
||||||
att_cache_buffer: torch.Tensor,
|
att_cache_buffer: torch.Tensor,
|
||||||
valid_lengths: list[int],
|
valid_lengths: list[int] | torch.Tensor,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""Run one padded DiT batch while capturing exact per-row CNN state."""
|
"""Run one padded DiT batch while capturing exact per-row CNN state.
|
||||||
lengths = torch.tensor(
|
|
||||||
(*valid_lengths, *valid_lengths),
|
``valid_lengths`` is one length per request, or the ``(2B,)`` CFG
|
||||||
device=estimator_input.device,
|
lengths tensor a CUDA graph captures as a static input. Upstream's
|
||||||
dtype=torch.long,
|
``DiTBlock.forward_chunk``, with attention over the output cache in
|
||||||
)
|
place (``_attend_in_place``) and the causal convolutions on the
|
||||||
|
frames-major activations (``_causal_conv_frames_major``).
|
||||||
|
"""
|
||||||
|
if isinstance(valid_lengths, torch.Tensor):
|
||||||
|
lengths = valid_lengths
|
||||||
|
else:
|
||||||
|
lengths = torch.tensor(
|
||||||
|
(*valid_lengths, *valid_lengths),
|
||||||
|
device=estimator_input.device,
|
||||||
|
dtype=torch.long,
|
||||||
|
)
|
||||||
x = estimator.in_proj(estimator_input.transpose(1, 2))
|
x = estimator.in_proj(estimator_input.transpose(1, 2))
|
||||||
for block_index, block in enumerate(estimator.blocks):
|
for block_index, block in enumerate(estimator.blocks):
|
||||||
(
|
(
|
||||||
@@ -622,49 +777,55 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
) = block.adaLN_modulation(time_embedding).chunk(9, dim=-1)
|
) = block.adaLN_modulation(time_embedding).chunk(9, dim=-1)
|
||||||
|
|
||||||
normalized = block.norm1(x) * (1 + scale_msa) + shift_msa
|
normalized = block.norm1(x) * (1 + scale_msa) + shift_msa
|
||||||
x_att, new_att = block.attn.forward_chunk(
|
x_att = BatchedToken2Wav._attend_in_place(
|
||||||
|
block.attn,
|
||||||
normalized,
|
normalized,
|
||||||
att_cache[block_index],
|
att_cache[block_index],
|
||||||
attn_mask,
|
attn_mask,
|
||||||
|
att_cache_buffer[block_index],
|
||||||
)
|
)
|
||||||
x = x + gate_msa * x_att
|
x = x + gate_msa * x_att
|
||||||
|
|
||||||
|
# CausalConvBlock: [T, conv1, T, LayerNorm, Mish, T, conv2, T]
|
||||||
|
# with the transposes (T) dropped.
|
||||||
|
conv = block.conv
|
||||||
conv_input = block.norm3(x) * (1 + scale_conv) + shift_conv
|
conv_input = block.norm3(x) * (1 + scale_conv) + shift_conv
|
||||||
old_cnn = cnn_cache[block_index]
|
old_cnn = cnn_cache[block_index]
|
||||||
if old_cnn is None:
|
if old_cnn is None:
|
||||||
width = int(block.conv.block[1].causal_padding[0])
|
|
||||||
old_cnn = conv_input.new_zeros(
|
old_cnn = conv_input.new_zeros(
|
||||||
(
|
(
|
||||||
int(conv_input.shape[0]),
|
int(conv_input.shape[0]),
|
||||||
int(block.conv.in_channels + block.conv.out_channels),
|
int(conv.in_channels + conv.out_channels),
|
||||||
width,
|
int(conv.block[1].causal_padding[0]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
old_cnn1, old_cnn2 = old_cnn.split((block.conv.in_channels, block.conv.out_channels), dim=1)
|
width = int(old_cnn.shape[2])
|
||||||
|
history = torch.cat((old_cnn[:, : conv.in_channels].transpose(1, 2), conv_input), dim=1)
|
||||||
conv1_input = block.conv.block[0](conv_input)
|
new_cnn1 = BatchedToken2Wav._gather_causal_cache(history, lengths, width)
|
||||||
conv1_output, _ = block.conv.block[1].forward_chunk(conv1_input, old_cnn1)
|
hidden = conv.block[4](conv.block[3](BatchedToken2Wav._causal_conv_frames_major(conv.block[1], history)))
|
||||||
new_cnn1 = self._gather_causal_cache(
|
history = torch.cat((old_cnn[:, conv.in_channels :].transpose(1, 2), hidden), dim=1)
|
||||||
torch.cat((old_cnn1, conv1_input), dim=2),
|
new_cnn2 = BatchedToken2Wav._gather_causal_cache(history, lengths, width)
|
||||||
lengths,
|
x = x + gate_conv * BatchedToken2Wav._causal_conv_frames_major(conv.block[6], history)
|
||||||
int(old_cnn1.shape[2]),
|
|
||||||
)
|
|
||||||
|
|
||||||
conv2_input = block.conv.block[2:6](conv1_output)
|
|
||||||
conv2_output, _ = block.conv.block[6].forward_chunk(conv2_input, old_cnn2)
|
|
||||||
new_cnn2 = self._gather_causal_cache(
|
|
||||||
torch.cat((old_cnn2, conv2_input), dim=2),
|
|
||||||
lengths,
|
|
||||||
int(old_cnn2.shape[2]),
|
|
||||||
)
|
|
||||||
x = x + gate_conv * block.conv.block[7](conv2_output)
|
|
||||||
x = x + gate_mlp * block.mlp(block.norm2(x) * (1 + scale_mlp) + shift_mlp)
|
x = x + gate_mlp * block.mlp(block.norm2(x) * (1 + scale_mlp) + shift_mlp)
|
||||||
|
|
||||||
cnn_cache_buffer[block_index].copy_(torch.cat((new_cnn1, new_cnn2), dim=1))
|
cnn_cache_buffer[block_index][:, : conv.in_channels].copy_(new_cnn1)
|
||||||
att_cache_buffer[block_index][:, :, : int(new_att.shape[2]), :].copy_(new_att)
|
cnn_cache_buffer[block_index][:, conv.in_channels :].copy_(new_cnn2)
|
||||||
|
|
||||||
return estimator.final_layer(x, time_embedding).transpose(1, 2)
|
return estimator.final_layer(x, time_embedding).transpose(1, 2)
|
||||||
|
|
||||||
|
def _get_timeline(self, device: torch.device, dtype: torch.dtype) -> tuple[torch.Tensor, list[float]]:
|
||||||
|
"""The Euler timeline and its step sizes, read to the host once.
|
||||||
|
|
||||||
|
``_euler_step`` takes ``dt`` as a Python float, so reading it
|
||||||
|
from the device timeline on every decode would sync once per step.
|
||||||
|
"""
|
||||||
|
key = (device, dtype)
|
||||||
|
cached = self._timeline_cache.get(key)
|
||||||
|
if cached is None:
|
||||||
|
cached = _euler_timeline(self.n_timesteps, device, dtype)
|
||||||
|
self._timeline_cache[key] = cached
|
||||||
|
return cached
|
||||||
|
|
||||||
def _decode_cfm(
|
def _decode_cfm(
|
||||||
self,
|
self,
|
||||||
mu: torch.Tensor,
|
mu: torch.Tensor,
|
||||||
@@ -672,30 +833,44 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
cond: torch.Tensor,
|
cond: torch.Tensor,
|
||||||
*,
|
*,
|
||||||
cnn_cache: torch.Tensor | None,
|
cnn_cache: torch.Tensor | None,
|
||||||
att_cache: torch.Tensor | None,
|
att_cache: torch.Tensor | list[torch.Tensor] | None,
|
||||||
valid_lengths: list[int] | None = None,
|
valid_lengths: list[int] | None = None,
|
||||||
|
att_keep: tuple[int, int] | None = None,
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
torch.Tensor,
|
torch.Tensor,
|
||||||
torch.Tensor,
|
torch.Tensor,
|
||||||
torch.Tensor | list[torch.Tensor],
|
torch.Tensor | list[torch.Tensor],
|
||||||
]:
|
]:
|
||||||
|
"""Run the CFM Euler solve for one chunk.
|
||||||
|
|
||||||
|
``att_cache`` may be the stacked estimator cache or one tensor per
|
||||||
|
request (the Whole-Euler path). A Whole-Euler solve returns the new
|
||||||
|
cache in the same form, already trimmed by ``att_keep``; the eager
|
||||||
|
path returns it stacked and untrimmed (ragged: per request, untrimmed).
|
||||||
|
"""
|
||||||
decoder = self.flow.decoder
|
decoder = self.flow.decoder
|
||||||
estimator = decoder.estimator
|
estimator = decoder.estimator
|
||||||
batch_size = int(mu.shape[0])
|
batch_size = int(mu.shape[0])
|
||||||
offset = int(att_cache.shape[4]) if att_cache is not None else 0
|
att_rows = att_cache if isinstance(att_cache, list) else None
|
||||||
|
if att_rows is not None:
|
||||||
|
offset = int(att_rows[0].shape[4])
|
||||||
|
else:
|
||||||
|
offset = int(att_cache.shape[4]) if att_cache is not None else 0
|
||||||
mel_frames = int(mu.shape[2])
|
mel_frames = int(mu.shape[2])
|
||||||
|
graphs_active = valid_lengths is None and (
|
||||||
|
self._whole_euler_active()
|
||||||
|
or (
|
||||||
|
self._cfm_graph_wrapper is not None
|
||||||
|
and getattr(self._cfm_graph_wrapper, "enabled", True)
|
||||||
|
and self._trt_stepper is None
|
||||||
|
)
|
||||||
|
)
|
||||||
pad_frames = _cfm_pad_frames(
|
pad_frames = _cfm_pad_frames(
|
||||||
mel_frames=mel_frames,
|
mel_frames=mel_frames,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
noise_capacity=int(decoder.rand_noise.shape[2]),
|
noise_capacity=int(decoder.rand_noise.shape[2]),
|
||||||
bucket_frames=self._cfm_graph_bucket_frames,
|
bucket_frames=self._cfm_graph_bucket_frames,
|
||||||
disabled=(
|
disabled=(valid_lengths is not None or not graphs_active),
|
||||||
valid_lengths is not None
|
|
||||||
or self._cfm_graph_wrapper is None
|
|
||||||
# `_disable` keeps the wrapper object alive, so check the flag
|
|
||||||
# too: padding under a disabled wrapper is pure overhead.
|
|
||||||
or not self._cfm_graph_wrapper.enabled
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if pad_frames:
|
if pad_frames:
|
||||||
# Replicate rather than zero: a repeated last frame is a closer
|
# Replicate rather than zero: a repeated last frame is a closer
|
||||||
@@ -718,15 +893,7 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
# is what removes them; zeroing alone would not, since a zero row
|
# is what removes them; zeroing alone would not, since a zero row
|
||||||
# still occupies part of the softmax denominator.
|
# still occupies part of the softmax denominator.
|
||||||
x[:, :, mel_frames:] = 0.0
|
x[:, :, mel_frames:] = 0.0
|
||||||
timeline = torch.linspace(
|
timeline, dt_steps = self._get_timeline(mu.device, mu.dtype)
|
||||||
0,
|
|
||||||
1,
|
|
||||||
self.n_timesteps + 1,
|
|
||||||
device=mu.device,
|
|
||||||
dtype=mu.dtype,
|
|
||||||
)
|
|
||||||
timeline = 1 - torch.cos(timeline * 0.5 * torch.pi)
|
|
||||||
time = timeline[0].expand(batch_size)
|
|
||||||
mu_cfg = torch.cat((mu, torch.zeros_like(mu)), dim=0)
|
mu_cfg = torch.cat((mu, torch.zeros_like(mu)), dim=0)
|
||||||
speakers_cfg = torch.cat((speakers, torch.zeros_like(speakers)), dim=0)
|
speakers_cfg = torch.cat((speakers, torch.zeros_like(speakers)), dim=0)
|
||||||
cond_cfg = torch.cat((cond, torch.zeros_like(cond)), dim=0)
|
cond_cfg = torch.cat((cond, torch.zeros_like(cond)), dim=0)
|
||||||
@@ -761,19 +928,42 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
device=mu.device,
|
device=mu.device,
|
||||||
)
|
)
|
||||||
attn_mask[:, :, mel_frames : mel_frames + pad_frames] = False
|
attn_mask[:, :, mel_frames : mel_frames + pad_frames] = False
|
||||||
|
|
||||||
|
if self._whole_euler_active() and (valid_lengths is None or self._whole_euler_ragged_active()):
|
||||||
|
whole_euler_result = self._whole_euler_graph_wrapper.replay(
|
||||||
|
x=x,
|
||||||
|
mu_cfg=mu_cfg,
|
||||||
|
speakers_cfg=speakers_cfg,
|
||||||
|
cond_cfg=cond_cfg,
|
||||||
|
cnn_cache=cnn_cache,
|
||||||
|
att_cache=att_cache,
|
||||||
|
attn_mask=attn_mask,
|
||||||
|
mel_frames=mel_frames,
|
||||||
|
pad_frames=pad_frames,
|
||||||
|
valid_lengths=valid_lengths,
|
||||||
|
att_keep=att_keep,
|
||||||
|
)
|
||||||
|
if whole_euler_result is not None:
|
||||||
|
return whole_euler_result
|
||||||
|
if att_rows is not None:
|
||||||
|
att_cache = self._stack_estimator_att(att_rows)
|
||||||
|
|
||||||
next_cnn: list[torch.Tensor] = []
|
next_cnn: list[torch.Tensor] = []
|
||||||
next_att_cache: torch.Tensor | None = None
|
next_att_cache: torch.Tensor | None = None
|
||||||
ragged_att_cache: list[torch.Tensor] | None = None
|
ragged_att_cache: list[torch.Tensor] | None = None
|
||||||
dt = timeline[1] - timeline[0]
|
time_embeddings = [
|
||||||
|
estimator.t_embedder(timeline[s].expand(2 * batch_size)).unsqueeze(1) for s in range(self.n_timesteps)
|
||||||
|
]
|
||||||
with _token2wav_sdpa_context(mu.device):
|
with _token2wav_sdpa_context(mu.device):
|
||||||
for step in range(self.n_timesteps):
|
for step in range(self.n_timesteps):
|
||||||
old_cnn = cnn_cache[step] if cnn_cache is not None else None
|
old_cnn = cnn_cache[step] if cnn_cache is not None else None
|
||||||
old_att = att_cache[step] if att_cache is not None else None
|
old_att = att_cache[step] if att_cache is not None else None
|
||||||
|
step_time = timeline[step].expand(2 * batch_size)
|
||||||
estimate, step_cnn, step_att = self._estimator_step(
|
estimate, step_cnn, step_att = self._estimator_step(
|
||||||
estimator,
|
estimator,
|
||||||
x=torch.cat((x, x), dim=0),
|
x=torch.cat((x, x), dim=0),
|
||||||
mu=mu_cfg,
|
mu=mu_cfg,
|
||||||
time=torch.cat((time, time), dim=0),
|
time=step_time,
|
||||||
speakers=speakers_cfg,
|
speakers=speakers_cfg,
|
||||||
cond=cond_cfg,
|
cond=cond_cfg,
|
||||||
cnn_cache=old_cnn,
|
cnn_cache=old_cnn,
|
||||||
@@ -781,16 +971,13 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
attn_mask=attn_mask,
|
attn_mask=attn_mask,
|
||||||
valid_lengths=valid_lengths,
|
valid_lengths=valid_lengths,
|
||||||
valid_frames=mel_frames if pad_frames else None,
|
valid_frames=mel_frames if pad_frames else None,
|
||||||
|
time_embedding=time_embeddings[step],
|
||||||
)
|
)
|
||||||
if pad_frames:
|
if pad_frames:
|
||||||
_zero_padded_cnn_cache(step_cnn, estimator, pad_frames)
|
_zero_padded_cnn_cache(step_cnn, estimator, pad_frames)
|
||||||
conditional, unconditional = estimate.split(batch_size, dim=0)
|
x = _euler_step(x, estimate, dt_steps[step], decoder.inference_cfg_rate, batch_size)
|
||||||
velocity = (1.0 + decoder.inference_cfg_rate) * conditional - decoder.inference_cfg_rate * unconditional
|
if pad_frames:
|
||||||
x = x + dt * velocity
|
_zero_padded_frames(x, mel_frames)
|
||||||
_zero_padded_frames(x, mel_frames if pad_frames else None)
|
|
||||||
time = time + dt
|
|
||||||
if step + 1 < self.n_timesteps:
|
|
||||||
dt = timeline[step + 2] - time[0]
|
|
||||||
next_cnn.append(step_cnn)
|
next_cnn.append(step_cnn)
|
||||||
if valid_lengths is not None:
|
if valid_lengths is not None:
|
||||||
if ragged_att_cache is None:
|
if ragged_att_cache is None:
|
||||||
@@ -842,28 +1029,40 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
assert next_att_cache is not None
|
assert next_att_cache is not None
|
||||||
return x, torch.stack(next_cnn), next_att_cache
|
return x, torch.stack(next_cnn), next_att_cache
|
||||||
|
|
||||||
def _split_flow_cache(self, cache: dict[str, torch.Tensor], batch_size: int) -> list[dict[str, torch.Tensor]]:
|
def _split_flow_cache(
|
||||||
|
self,
|
||||||
|
cache: dict[str, Any],
|
||||||
|
batch_size: int,
|
||||||
|
*,
|
||||||
|
estimator_att_keep: tuple[int, int] | None = None,
|
||||||
|
) -> list[dict[str, torch.Tensor]]:
|
||||||
|
"""Per-request flow caches from a batch's stacked caches.
|
||||||
|
|
||||||
|
``estimator_att_cache`` is stacked ``(n_t, depth, 2B, heads, L, d)``
|
||||||
|
with CFG rows ``[cond x B | uncond x B]``, or already one tensor per
|
||||||
|
request (Whole-Euler). The streaming trim ``estimator_att_keep`` is
|
||||||
|
applied while each request is copied out, so no trimmed copy of the
|
||||||
|
whole batch is built first: at 16 requests that copy alone was ~7 GiB.
|
||||||
|
"""
|
||||||
|
estimator_att = cache["estimator_att_cache"]
|
||||||
|
stacked_att = isinstance(estimator_att, torch.Tensor)
|
||||||
|
if stacked_att:
|
||||||
|
keep_ranges = _att_keep_ranges(int(estimator_att.shape[4]), estimator_att_keep)
|
||||||
|
request_shape = list(estimator_att.shape)
|
||||||
|
request_shape[2] = 2
|
||||||
|
request_shape[4] = sum(length for _, length in keep_ranges)
|
||||||
|
att_rows = estimator_att.unflatten(2, (2, batch_size))
|
||||||
result: list[dict[str, torch.Tensor]] = []
|
result: list[dict[str, torch.Tensor]] = []
|
||||||
for row in range(batch_size):
|
for row in range(batch_size):
|
||||||
estimator_att = cache["estimator_att_cache"]
|
if stacked_att:
|
||||||
if estimator_att.dtype == self._estimator_att_cache_dtype:
|
|
||||||
request_att = torch.cat(
|
|
||||||
(
|
|
||||||
estimator_att[:, :, row : row + 1],
|
|
||||||
estimator_att[:, :, batch_size + row : batch_size + row + 1],
|
|
||||||
),
|
|
||||||
dim=2,
|
|
||||||
).detach()
|
|
||||||
else:
|
|
||||||
request_shape = list(estimator_att.shape)
|
|
||||||
request_shape[2] = 2
|
|
||||||
request_att = torch.empty(
|
request_att = torch.empty(
|
||||||
request_shape,
|
request_shape,
|
||||||
device=estimator_att.device,
|
device=estimator_att.device,
|
||||||
dtype=self._estimator_att_cache_dtype,
|
dtype=self._estimator_att_cache_dtype,
|
||||||
)
|
)
|
||||||
request_att[:, :, 0:1].copy_(estimator_att[:, :, row : row + 1])
|
_copy_frame_segments(request_att, att_rows[:, :, :, row], keep_ranges)
|
||||||
request_att[:, :, 1:2].copy_(estimator_att[:, :, batch_size + row : batch_size + row + 1])
|
else:
|
||||||
|
request_att = estimator_att[row]
|
||||||
result.append(
|
result.append(
|
||||||
{
|
{
|
||||||
"conformer_cnn_cache": cache["conformer_cnn_cache"][row : row + 1].detach().clone(),
|
"conformer_cnn_cache": cache["conformer_cnn_cache"][row : row + 1].detach().clone(),
|
||||||
@@ -880,20 +1079,45 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _stack_flow_cache(self, states: list[BatchedToken2WavState]) -> dict[str, torch.Tensor]:
|
@staticmethod
|
||||||
|
def _stack_estimator_att(rows: list[torch.Tensor]) -> torch.Tensor:
|
||||||
|
"""Stack per-request ``(n_t, depth, 2, ...)`` caches into CFG order ``[cond x B | uncond x B]``."""
|
||||||
|
return torch.cat((*[row[:, :, 0:1] for row in rows], *[row[:, :, 1:2] for row in rows]), dim=2)
|
||||||
|
|
||||||
|
def _stack_flow_cache(
|
||||||
|
self,
|
||||||
|
states: list[BatchedToken2WavState],
|
||||||
|
*,
|
||||||
|
include_estimator_att: bool = True,
|
||||||
|
) -> dict[str, torch.Tensor | None]:
|
||||||
|
"""Stack per-request flow caches for one batched call.
|
||||||
|
|
||||||
|
The estimator attention cache is by far the largest (~0.5 GiB per
|
||||||
|
request in fp32). The Whole-Euler path reads it per request, so it
|
||||||
|
skips stacking it (``include_estimator_att=False``).
|
||||||
|
"""
|
||||||
flows = [state.flow_cache for state in states]
|
flows = [state.flow_cache for state in states]
|
||||||
conditional_cnn = [flow["estimator_cnn_cache"][:, :, 0:1] for flow in flows]
|
conditional_cnn = [flow["estimator_cnn_cache"][:, :, 0:1] for flow in flows]
|
||||||
unconditional_cnn = [flow["estimator_cnn_cache"][:, :, 1:2] for flow in flows]
|
unconditional_cnn = [flow["estimator_cnn_cache"][:, :, 1:2] for flow in flows]
|
||||||
conditional_att = [flow["estimator_att_cache"][:, :, 0:1] for flow in flows]
|
|
||||||
unconditional_att = [flow["estimator_att_cache"][:, :, 1:2] for flow in flows]
|
|
||||||
estimator_att = torch.cat((*conditional_att, *unconditional_att), dim=2)
|
|
||||||
return {
|
return {
|
||||||
"conformer_cnn_cache": torch.cat([flow["conformer_cnn_cache"] for flow in flows], dim=0),
|
"conformer_cnn_cache": torch.cat([flow["conformer_cnn_cache"] for flow in flows], dim=0),
|
||||||
"conformer_att_cache": torch.cat([flow["conformer_att_cache"] for flow in flows], dim=1),
|
"conformer_att_cache": torch.cat([flow["conformer_att_cache"] for flow in flows], dim=1),
|
||||||
"estimator_cnn_cache": torch.cat((*conditional_cnn, *unconditional_cnn), dim=2),
|
"estimator_cnn_cache": torch.cat((*conditional_cnn, *unconditional_cnn), dim=2),
|
||||||
"estimator_att_cache": estimator_att,
|
"estimator_att_cache": (
|
||||||
|
self._stack_estimator_att([flow["estimator_att_cache"] for flow in flows])
|
||||||
|
if include_estimator_att
|
||||||
|
else None
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _whole_euler_active(self) -> bool:
|
||||||
|
wrapper = self._whole_euler_graph_wrapper
|
||||||
|
return wrapper is not None and wrapper.enabled and self._trt_stepper is None
|
||||||
|
|
||||||
|
def _whole_euler_ragged_active(self) -> bool:
|
||||||
|
"""Whether Whole-Euler graphs also solve ragged (per-row length) batches."""
|
||||||
|
return self._whole_euler_active() and self._whole_euler_graph_wrapper.ragged_body is not None
|
||||||
|
|
||||||
def _create_initial_states(
|
def _create_initial_states(
|
||||||
self,
|
self,
|
||||||
features: PromptFeatures,
|
features: PromptFeatures,
|
||||||
@@ -913,12 +1137,15 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
att_cache=None,
|
att_cache=None,
|
||||||
)
|
)
|
||||||
projected_speakers = self.flow.spk_embed_affine_layer(F.normalize(speakers, dim=1))
|
projected_speakers = self.flow.spk_embed_affine_layer(F.normalize(speakers, dim=1))
|
||||||
|
# The prompt cache never reaches the trim length; ``att_keep`` only
|
||||||
|
# tells the Whole-Euler arena the steady cache length to reserve.
|
||||||
_, estimator_cnn, estimator_att = self._decode_cfm(
|
_, estimator_cnn, estimator_att = self._decode_cfm(
|
||||||
hidden.transpose(1, 2).contiguous(),
|
hidden.transpose(1, 2).contiguous(),
|
||||||
projected_speakers,
|
projected_speakers,
|
||||||
prompt_mels.transpose(1, 2).contiguous(),
|
prompt_mels.transpose(1, 2).contiguous(),
|
||||||
cnn_cache=None,
|
cnn_cache=None,
|
||||||
att_cache=None,
|
att_cache=None,
|
||||||
|
att_keep=(int(prompt_mels.shape[1]), _CACHE_TRIM_SUFFIX),
|
||||||
)
|
)
|
||||||
flow_cache = {
|
flow_cache = {
|
||||||
"conformer_cnn_cache": conformer_cnn,
|
"conformer_cnn_cache": conformer_cnn,
|
||||||
@@ -941,7 +1168,17 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def setup_batch(self, features: PromptFeatures, batch_size: int) -> list[BatchedToken2WavState]:
|
def setup_batch(self, features: PromptFeatures, batch_size: int) -> list[BatchedToken2WavState]:
|
||||||
"""Reuse read-only prompt conditioning for the same shape and padding."""
|
"""Initial states for ``batch_size`` requests sharing one prompt.
|
||||||
|
|
||||||
|
Every request starts from the same prompt tokens, speaker, mels and
|
||||||
|
noise, so the prompt is solved once and all requests share that one
|
||||||
|
read-only state. Decoding writes a state only where Whole-Euler updates
|
||||||
|
an estimator cache in place, which needs a cache held by one request
|
||||||
|
and allocated with room for its new length; the prompt cache is shared
|
||||||
|
and allocated exactly, and the first chunk always grows it. Solving it
|
||||||
|
per batch size recomputed identical rows and cached one ~0.3 GiB
|
||||||
|
estimator cache copy per request.
|
||||||
|
"""
|
||||||
bucket_frames = (
|
bucket_frames = (
|
||||||
self._cfm_graph_bucket_frames
|
self._cfm_graph_bucket_frames
|
||||||
if self._cfm_graph_wrapper is not None and self._cfm_graph_wrapper.enabled
|
if self._cfm_graph_wrapper is not None and self._cfm_graph_wrapper.enabled
|
||||||
@@ -949,17 +1186,17 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
)
|
)
|
||||||
# Capture the policy before setup: graph capture may disable the wrapper
|
# Capture the policy before setup: graph capture may disable the wrapper
|
||||||
# after padding has already been chosen for these initial states.
|
# after padding has already been chosen for these initial states.
|
||||||
cache_key = (features.cache_key, batch_size, bucket_frames)
|
cache_key = (features.cache_key, bucket_frames)
|
||||||
cached = self._setup_cache.get(cache_key)
|
state = self._setup_cache.get(cache_key)
|
||||||
if cached is not None:
|
if state is not None:
|
||||||
self._setup_cache.move_to_end(cache_key)
|
self._setup_cache.move_to_end(cache_key)
|
||||||
return list(cached)
|
else:
|
||||||
states = self._create_initial_states(features, batch_size)
|
(state,) = self._create_initial_states(features, 1)
|
||||||
if self._setup_cache_size > 0:
|
if self._setup_cache_size > 0:
|
||||||
self._setup_cache[cache_key] = tuple(states)
|
self._setup_cache[cache_key] = state
|
||||||
while len(self._setup_cache) > self._setup_cache_size:
|
while len(self._setup_cache) > self._setup_cache_size:
|
||||||
self._setup_cache.popitem(last=False)
|
self._setup_cache.popitem(last=False)
|
||||||
return states
|
return [state] * batch_size
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fade_in_out(
|
def _fade_in_out(
|
||||||
@@ -1058,7 +1295,12 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
flush_encoder: bool = False,
|
flush_encoder: bool = False,
|
||||||
) -> tuple[list[torch.Tensor], list[BatchedToken2WavState]]:
|
) -> tuple[list[torch.Tensor], list[BatchedToken2WavState]]:
|
||||||
batch_size = int(tokens.shape[0])
|
batch_size = int(tokens.shape[0])
|
||||||
flow_cache = self._stack_flow_cache(states)
|
# Whole-Euler reads and writes each request's estimator cache directly,
|
||||||
|
# so the batch never holds a stacked copy of it.
|
||||||
|
per_request_att = self._whole_euler_active()
|
||||||
|
flow_cache = self._stack_flow_cache(states, include_estimator_att=not per_request_att)
|
||||||
|
prompt_len = int(features.mels.shape[1])
|
||||||
|
att_keep = (prompt_len, _CACHE_TRIM_SUFFIX)
|
||||||
speakers = features.speaker_embedding.expand(batch_size, -1)
|
speakers = features.speaker_embedding.expand(batch_size, -1)
|
||||||
with self._autocast(tokens.device):
|
with self._autocast(tokens.device):
|
||||||
hidden, conformer_cnn, conformer_att = self._encode_chunk(
|
hidden, conformer_cnn, conformer_att = self._encode_chunk(
|
||||||
@@ -1074,20 +1316,15 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
projected_speakers,
|
projected_speakers,
|
||||||
cond,
|
cond,
|
||||||
cnn_cache=flow_cache["estimator_cnn_cache"],
|
cnn_cache=flow_cache["estimator_cnn_cache"],
|
||||||
att_cache=flow_cache["estimator_att_cache"],
|
att_cache=(
|
||||||
|
[state.flow_cache["estimator_att_cache"] for state in states]
|
||||||
|
if per_request_att
|
||||||
|
else flow_cache["estimator_att_cache"]
|
||||||
|
),
|
||||||
|
att_keep=att_keep,
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt_len = int(features.mels.shape[1])
|
conformer_att = _trim_streaming_cache(conformer_att, prompt_len)
|
||||||
if estimator_att.shape[4] > prompt_len + 100:
|
|
||||||
estimator_att = torch.cat(
|
|
||||||
(estimator_att[..., :prompt_len, :], estimator_att[..., -100:, :]),
|
|
||||||
dim=4,
|
|
||||||
)
|
|
||||||
if conformer_att.shape[3] > prompt_len + 100:
|
|
||||||
conformer_att = torch.cat(
|
|
||||||
(conformer_att[..., :prompt_len, :], conformer_att[..., -100:, :]),
|
|
||||||
dim=3,
|
|
||||||
)
|
|
||||||
new_flow = self._split_flow_cache(
|
new_flow = self._split_flow_cache(
|
||||||
{
|
{
|
||||||
"conformer_cnn_cache": conformer_cnn,
|
"conformer_cnn_cache": conformer_cnn,
|
||||||
@@ -1096,6 +1333,7 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
"estimator_att_cache": estimator_att,
|
"estimator_att_cache": estimator_att,
|
||||||
},
|
},
|
||||||
batch_size,
|
batch_size,
|
||||||
|
estimator_att_keep=att_keep,
|
||||||
)
|
)
|
||||||
old_mel = torch.cat([state.hift_cache["mel"] for state in states], dim=0)
|
old_mel = torch.cat([state.hift_cache["mel"] for state in states], dim=0)
|
||||||
old_source = torch.cat([state.hift_cache["source"] for state in states], dim=0)
|
old_source = torch.cat([state.hift_cache["source"] for state in states], dim=0)
|
||||||
@@ -1164,10 +1402,17 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
)
|
)
|
||||||
|
|
||||||
max_frames = self._max_encode_token_frames(states)
|
max_frames = self._max_encode_token_frames(states)
|
||||||
if any(int(row_tokens.numel()) > max_frames for row_tokens in tokens):
|
per_request_att = self._whole_euler_ragged_active()
|
||||||
|
if (self._whole_euler_active() and not per_request_att) or any(
|
||||||
|
int(row_tokens.numel()) > max_frames for row_tokens in tokens
|
||||||
|
):
|
||||||
# decode_batch owns the RelPos-safe encoder slicing used by
|
# decode_batch owns the RelPos-safe encoder slicing used by
|
||||||
# non-async Talker dumps. Preserve that path for overlong rows;
|
# non-async Talker dumps. Preserve that path for overlong rows.
|
||||||
# ordinary short duplex chunks continue through one padded DiT.
|
# Whole-Euler graphs that cannot run the ragged kernel also take
|
||||||
|
# this path: under concurrency most batches hold some request's
|
||||||
|
# short final chunk, and one padded eager DiT would run the whole
|
||||||
|
# batch ungraphed on a stacked copy of every cache. Otherwise short
|
||||||
|
# duplex chunks share one padded DiT solve, graphed when possible.
|
||||||
exact_groups: dict[tuple[int, bool], list[int]] = {}
|
exact_groups: dict[tuple[int, bool], list[int]] = {}
|
||||||
for row, (row_tokens, last_chunk) in enumerate(zip(tokens, last_chunks, strict=True)):
|
for row, (row_tokens, last_chunk) in enumerate(zip(tokens, last_chunks, strict=True)):
|
||||||
exact_groups.setdefault((int(row_tokens.numel()), last_chunk), []).append(row)
|
exact_groups.setdefault((int(row_tokens.numel()), last_chunk), []).append(row)
|
||||||
@@ -1193,7 +1438,8 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
conformer_att_rows: list[torch.Tensor | None] = [None] * batch_size
|
conformer_att_rows: list[torch.Tensor | None] = [None] * batch_size
|
||||||
for (_, last_chunk), rows in encoder_groups.items():
|
for (_, last_chunk), rows in encoder_groups.items():
|
||||||
group_states = [states[row] for row in rows]
|
group_states = [states[row] for row in rows]
|
||||||
group_cache = self._stack_flow_cache(group_states)
|
# The encoder reads only the conformer caches.
|
||||||
|
group_cache = self._stack_flow_cache(group_states, include_estimator_att=False)
|
||||||
group_tokens = torch.stack([tokens[row] for row in rows], dim=0)
|
group_tokens = torch.stack([tokens[row] for row in rows], dim=0)
|
||||||
with self._autocast(group_tokens.device):
|
with self._autocast(group_tokens.device):
|
||||||
hidden, conformer_cnn, conformer_att = self._encode_chunk(
|
hidden, conformer_cnn, conformer_att = self._encode_chunk(
|
||||||
@@ -1219,7 +1465,10 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
for row, hidden in enumerate(resolved_hidden):
|
for row, hidden in enumerate(resolved_hidden):
|
||||||
padded_hidden[row, : int(hidden.shape[1])].copy_(hidden[0])
|
padded_hidden[row, : int(hidden.shape[1])].copy_(hidden[0])
|
||||||
|
|
||||||
flow_cache = self._stack_flow_cache(states)
|
# A graphed ragged solve reads and writes each request's estimator
|
||||||
|
# cache directly (trimmed on the way out), like _decode_batch_once.
|
||||||
|
flow_cache = self._stack_flow_cache(states, include_estimator_att=not per_request_att)
|
||||||
|
prompt_len = int(features.mels.shape[1])
|
||||||
speakers = features.speaker_embedding.expand(batch_size, -1)
|
speakers = features.speaker_embedding.expand(batch_size, -1)
|
||||||
with self._autocast(padded_hidden.device):
|
with self._autocast(padded_hidden.device):
|
||||||
projected_speakers = self.flow.spk_embed_affine_layer(F.normalize(speakers, dim=1))
|
projected_speakers = self.flow.spk_embed_affine_layer(F.normalize(speakers, dim=1))
|
||||||
@@ -1229,33 +1478,24 @@ class BatchedToken2Wav(nn.Module):
|
|||||||
projected_speakers,
|
projected_speakers,
|
||||||
cond,
|
cond,
|
||||||
cnn_cache=flow_cache["estimator_cnn_cache"],
|
cnn_cache=flow_cache["estimator_cnn_cache"],
|
||||||
att_cache=flow_cache["estimator_att_cache"],
|
att_cache=(
|
||||||
|
[state.flow_cache["estimator_att_cache"] for state in states]
|
||||||
|
if per_request_att
|
||||||
|
else flow_cache["estimator_att_cache"]
|
||||||
|
),
|
||||||
valid_lengths=hidden_lengths,
|
valid_lengths=hidden_lengths,
|
||||||
|
att_keep=(prompt_len, _CACHE_TRIM_SUFFIX),
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt_len = int(features.mels.shape[1])
|
|
||||||
assert isinstance(estimator_att, list)
|
assert isinstance(estimator_att, list)
|
||||||
new_flow: list[dict[str, torch.Tensor]] = []
|
new_flow: list[dict[str, torch.Tensor]] = []
|
||||||
for row, row_estimator_att in enumerate(estimator_att):
|
for row, row_estimator_att in enumerate(estimator_att):
|
||||||
conformer_cnn = conformer_cnn_rows[row]
|
conformer_cnn = conformer_cnn_rows[row]
|
||||||
conformer_att = conformer_att_rows[row]
|
conformer_att = conformer_att_rows[row]
|
||||||
assert conformer_cnn is not None and conformer_att is not None
|
assert conformer_cnn is not None and conformer_att is not None
|
||||||
if row_estimator_att.shape[4] > prompt_len + 100:
|
# A graphed solve already trimmed the estimator cache; trimming is idempotent.
|
||||||
row_estimator_att = torch.cat(
|
row_estimator_att = _trim_streaming_cache(row_estimator_att, prompt_len)
|
||||||
(
|
conformer_att = _trim_streaming_cache(conformer_att, prompt_len)
|
||||||
row_estimator_att[..., :prompt_len, :],
|
|
||||||
row_estimator_att[..., -100:, :],
|
|
||||||
),
|
|
||||||
dim=4,
|
|
||||||
)
|
|
||||||
if conformer_att.shape[3] > prompt_len + 100:
|
|
||||||
conformer_att = torch.cat(
|
|
||||||
(
|
|
||||||
conformer_att[..., :prompt_len, :],
|
|
||||||
conformer_att[..., -100:, :],
|
|
||||||
),
|
|
||||||
dim=3,
|
|
||||||
)
|
|
||||||
new_flow.append(
|
new_flow.append(
|
||||||
{
|
{
|
||||||
"conformer_cnn_cache": conformer_cnn.detach().clone(),
|
"conformer_cnn_cache": conformer_cnn.detach().clone(),
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -268,14 +268,34 @@ class MiniCPMO45Code2Wav(nn.Module):
|
|||||||
raise ValueError(f"Invalid MiniCPM-o connector chunk configuration: {self._connector_config}")
|
raise ValueError(f"Invalid MiniCPM-o connector chunk configuration: {self._connector_config}")
|
||||||
raw_capture_batch_sizes = extra.get("hift_graph_capture_batch_sizes")
|
raw_capture_batch_sizes = extra.get("hift_graph_capture_batch_sizes")
|
||||||
capture_batch_sizes = [1] if raw_capture_batch_sizes is None else raw_capture_batch_sizes
|
capture_batch_sizes = [1] if raw_capture_batch_sizes is None else raw_capture_batch_sizes
|
||||||
|
max_serial_batch = extra.get("max_serial_batch")
|
||||||
|
max_serial_batch = 4 if max_serial_batch is None else int(max_serial_batch)
|
||||||
self._hift_graph_config = {
|
self._hift_graph_config = {
|
||||||
"enabled": bool(extra.get("enable_hift_graph", False)),
|
"enabled": bool(extra.get("enable_hift_graph", False)),
|
||||||
"capture_batch_sizes": capture_batch_sizes,
|
"capture_batch_sizes": capture_batch_sizes,
|
||||||
|
"max_serial_batch": max_serial_batch,
|
||||||
}
|
}
|
||||||
|
enable_whole_euler = extra.get("enable_whole_euler")
|
||||||
|
max_graph_batch_raw = extra.get("max_graph_batch")
|
||||||
|
max_graph_batch = int(max_graph_batch_raw) if max_graph_batch_raw is not None else None
|
||||||
|
micro_batch_size_raw = extra.get("micro_batch_size")
|
||||||
|
if micro_batch_size_raw is not None:
|
||||||
|
micro_batch_size = int(micro_batch_size_raw)
|
||||||
|
else:
|
||||||
|
# The Whole-Euler arena reserves one attention cache per micro-batch
|
||||||
|
# row, so size it for the most requests this stage ever batches.
|
||||||
|
max_num_seqs = getattr(getattr(vllm_config, "scheduler_config", None), "max_num_seqs", None)
|
||||||
|
micro_batch_size = min(int(max_num_seqs), max_graph_batch or 16) if max_num_seqs else None
|
||||||
self._cfm_graph_config = {
|
self._cfm_graph_config = {
|
||||||
"enabled": bool(extra.get("enable_cfm_graph", False)),
|
"enabled": bool(extra.get("enable_cfm_graph", False)),
|
||||||
"max_graphs": int(extra.get("cfm_max_graphs", 32)),
|
"max_graphs": int(extra.get("cfm_max_graphs", 32)),
|
||||||
"bucket_frames": int(extra.get("cfm_graph_bucket_frames", 0)),
|
"bucket_frames": int(extra.get("cfm_graph_bucket_frames", 0)),
|
||||||
|
"capture_frames": extra.get("cfm_graph_capture_frames"),
|
||||||
|
"enable_whole_euler": enable_whole_euler is None or bool(enable_whole_euler),
|
||||||
|
"max_serial_batch": max_serial_batch,
|
||||||
|
"max_graph_batch": max_graph_batch,
|
||||||
|
"micro_batch_size": micro_batch_size,
|
||||||
|
"pad_max_rows": extra.get("whole_euler_pad_max_rows"),
|
||||||
}
|
}
|
||||||
self._ref_max_seconds = float(extra.get("ref_audio_max_seconds", _REF_MAX_SECONDS))
|
self._ref_max_seconds = float(extra.get("ref_audio_max_seconds", _REF_MAX_SECONDS))
|
||||||
if self._ref_max_seconds <= 0:
|
if self._ref_max_seconds <= 0:
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ def _patched_estimator_step(
|
|||||||
attn_mask=None,
|
attn_mask=None,
|
||||||
valid_lengths=None,
|
valid_lengths=None,
|
||||||
valid_frames=None,
|
valid_frames=None,
|
||||||
|
time_embedding=None,
|
||||||
):
|
):
|
||||||
assert _original_estimator_step is not None
|
assert _original_estimator_step is not None
|
||||||
graph_runner = _backend_graph_runners.get(self)
|
graph_runner = _backend_graph_runners.get(self)
|
||||||
@@ -137,6 +138,7 @@ def _patched_estimator_step(
|
|||||||
attn_mask=attn_mask,
|
attn_mask=attn_mask,
|
||||||
valid_lengths=valid_lengths,
|
valid_lengths=valid_lengths,
|
||||||
valid_frames=valid_frames,
|
valid_frames=valid_frames,
|
||||||
|
time_embedding=time_embedding,
|
||||||
)
|
)
|
||||||
if (cnn_cache is None) != (att_cache is None):
|
if (cnn_cache is None) != (att_cache is None):
|
||||||
raise ValueError("estimator CNN and attention caches must both be present or absent")
|
raise ValueError("estimator CNN and attention caches must both be present or absent")
|
||||||
@@ -148,7 +150,8 @@ def _patched_estimator_step(
|
|||||||
|
|
||||||
# The upstream embedder creates a frequency tensor on the host. Keep it
|
# The upstream embedder creates a frequency tensor on the host. Keep it
|
||||||
# outside capture while retaining the tensor-only estimator body in graph.
|
# outside capture while retaining the tensor-only estimator body in graph.
|
||||||
time_embedding = estimator.t_embedder(time).unsqueeze(1)
|
if time_embedding is None:
|
||||||
|
time_embedding = estimator.t_embedder(time).unsqueeze(1)
|
||||||
if cnn_cache is None:
|
if cnn_cache is None:
|
||||||
return graph_runner.run(
|
return graph_runner.run(
|
||||||
"cfm_estimator",
|
"cfm_estimator",
|
||||||
|
|||||||
Reference in new issue
Block a user