[Diffusion][HunyuanImage3] Add request paged prefix caching (#7719)

Signed-off-by: zwhzzz0821 <2831474076@qq.com>
This commit is contained in:
Yukim1 authored and GitHub committed 2026-09-23 15:34:56 +00:00
1 parent 9a1a05facd
commit 205aa01c7b
49 files changed
+2889 -127

No files matched your search

@@ -0,0 +1,94 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Small diffusion identity helpers alongside vLLM's native KV cache utilities.
Content hashing runs before Scheduler admission, only with prefix caching on.
Requests carry native MultiModalFeatureSpec / PlaceholderRange values; native
generate_block_hash_extra_keys handles block intersections, with no per-token
extra-key expansion or intermediate dependency objects.
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import numpy as np
import torch
from vllm.multimodal.hasher import MultiModalHasher
if TYPE_CHECKING:
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
def _normalize_identity(value: object) -> object:
# MultiModalHasher handles tensor bytes (including BF16). Its generic
# recursive serialization alone does not canonicalize nested dict order
# or distinguish every container/scalar boundary. A typed JSON envelope
# supplies those guarantees and rejects the upstream pickle fallback.
if value is None:
return ["none"]
if isinstance(value, np.generic):
scalar = value.item()
if isinstance(scalar, np.generic):
# Extended-precision scalars have no lossless Python scalar;
# item() returns another NumPy scalar instead of making progress.
raise TypeError(f"Unsupported prefix-cache identity scalar dtype: {value.dtype}")
return _normalize_identity(scalar)
if isinstance(value, bool):
return ["bool", value]
if isinstance(value, int):
return ["int", str(value)]
if isinstance(value, float):
return ["float", value.hex()]
if isinstance(value, str):
return ["str", value]
if isinstance(value, bytes):
return ["bytes", value.hex()]
if isinstance(value, torch.Tensor):
if value.layout != torch.strided or value.is_quantized:
raise TypeError("Prefix-cache identity requires a dense, non-quantized tensor")
tensor = value.detach().cpu().contiguous()
# A byte view also covers dtypes NumPy cannot represent directly.
data = tensor.reshape(-1).view(torch.uint8).numpy()
return ["tensor", str(tensor.dtype), list(tensor.shape), MultiModalHasher.hash_kwargs("sha256", data=data)]
if isinstance(value, np.ndarray):
if value.dtype.hasobject or value.dtype.fields is not None:
raise TypeError("Prefix-cache identity does not support object or structured arrays")
return [
"array",
value.dtype.str,
list(value.shape),
MultiModalHasher.hash_kwargs("sha256", data=np.ascontiguousarray(value)),
]
if isinstance(value, dict):
if any(not isinstance(key, str) for key in value):
raise TypeError("Prefix-cache identity dictionaries require string keys")
return ["dict", [[key, _normalize_identity(value[key])] for key in sorted(value)]]
if isinstance(value, list | tuple):
return ["sequence", [_normalize_identity(item) for item in value]]
raise TypeError(f"Unsupported prefix-cache identity value: {type(value).__name__}")
def hash_prefix_cache_value(value: object) -> bytes:
"""Deterministic, type-framed identity using vLLM's multimodal hasher."""
encoded = json.dumps(_normalize_identity(value), ensure_ascii=True, separators=(",", ":"))
return bytes.fromhex(MultiModalHasher.hash_kwargs("sha256", identity=encoded))
def get_cache_namespace(model_namespace: str, sampling: OmniDiffusionSamplingParams) -> str:
"""Isolate model semantics and the adapter actually activated by Worker.
Native LoRA block keys use lora_name, but diffusion also supports a scale
and identifies loaded adapters by lora_int_id. Keep that difference here.
Seeds are NOT global identity inputs: models attach random state only to
the multimodal features whose computation depends on it.
"""
if not isinstance(model_namespace, str) or not model_namespace:
raise ValueError("Model cache namespace must be non-empty")
lora = sampling.lora_request
adapter = None if lora is None else (lora.lora_int_id, float(sampling.lora_scale))
return hash_prefix_cache_value(("diffusion-prefix-identity-v2", model_namespace, adapter)).hex()
+138 -22
View File
@@ -1,11 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
from collections.abc import Sequence
from vllm.v1.core.kv_cache_manager import KVCacheManager
from vllm.utils.hashing import get_hash_fn_by_name
from vllm.v1.core.kv_cache_manager import KVCacheBlocks, KVCacheManager
from vllm.v1.core.kv_cache_utils import KVCacheBlock
from vllm.v1.kv_cache_interface import KVCacheConfig
from vllm_omni.core.sched.utils import free_kv_blocks_in_physical_order
@@ -31,6 +33,8 @@ class DiffusionKVCacheManager:
scheduler_block_size: int,
hash_block_size: int,
max_in_flight_tokens: int | None = None,
enable_prefix_caching: bool = False,
prefix_caching_hash_algo: str = "sha256",
) -> None:
if max_model_len <= 0:
raise ValueError(f"max_model_len must be positive, got {max_model_len}")
@@ -40,15 +44,18 @@ class DiffusionKVCacheManager:
raise ValueError("scheduler_block_size and hash_block_size must be positive")
if max_in_flight_tokens is not None and max_in_flight_tokens <= 0:
raise ValueError(f"max_in_flight_tokens must be positive when provided, got {max_in_flight_tokens}")
self._hash_function = get_hash_fn_by_name(prefix_caching_hash_algo)
self.native_manager = KVCacheManager(
kv_cache_config=kv_cache_config,
max_model_len=max_model_len,
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
max_in_flight_tokens=max_in_flight_tokens,
enable_caching=False,
enable_caching=enable_prefix_caching,
)
self.max_model_len = max_model_len
self.hash_block_size = hash_block_size
self.enable_prefix_caching = enable_prefix_caching
# Native vLLM may reserve a null block, so an idle BlockPool does not
# necessarily report ``kv_cache_config.num_blocks`` free blocks.
self._empty_pool_num_free_blocks = self.native_manager.block_pool.get_num_free_blocks()
@@ -57,31 +64,87 @@ class DiffusionKVCacheManager:
self._internal_request_ids: set[str] = set()
self._next_allocation_generation = 1
def _get_empty_pool_required_blocks(self, requests: Sequence[DiffusionKVRequest]) -> int:
"""Return the native full-sequence reservation for one public request.
def _prepare_block_hashes(self, requests: Sequence[DiffusionKVRequest]) -> None:
if not self.enable_prefix_caching:
return
for request in requests:
request.build_block_hashes(self.hash_block_size, self._hash_function)
vLLM performs this calculation inside ``allocate_slots`` when
``full_sequence_must_fit`` is enabled. Diffusion CFG adds one semantic
requirement that native requests do not have: every execution sequence
must fit atomically. Sum the native per-sequence result so a request
that can never fit is rejected independently of current pool usage.
def _clip_computed_blocks(
self,
blocks: KVCacheBlocks,
*,
hit_len: int,
cached_prefix_len: int,
) -> KVCacheBlocks:
if cached_prefix_len == hit_len:
return blocks
clipped_groups: list[list[KVCacheBlock]] = []
for group_blocks, cache_group in zip(
blocks.blocks,
self.native_manager.kv_cache_config.kv_cache_groups,
strict=True,
):
block_size = int(cache_group.kv_cache_spec.block_size)
if cached_prefix_len % block_size:
raise RuntimeError(
"Diffusion prefix hit is not aligned to a native cache group: "
f"cached_prefix_len={cached_prefix_len}, block_size={block_size}"
)
keep_blocks = cached_prefix_len // block_size
if keep_blocks > len(group_blocks):
raise RuntimeError(
"Native prefix lookup returned too few blocks for the common CFG boundary: "
f"required={keep_blocks}, returned={len(group_blocks)}"
)
clipped_groups.append(list(group_blocks[:keep_blocks]))
return self.native_manager.create_kv_cache_blocks(tuple(clipped_groups))
@staticmethod
def _unique_blocks(block_sets: Sequence[KVCacheBlocks]) -> list[KVCacheBlock]:
unique: dict[int, KVCacheBlock] = {}
for blocks in block_sets:
for group in blocks.blocks:
for block in group:
if not block.is_null:
unique[id(block)] = block
return list(unique.values())
def _get_empty_pool_required_blocks(
self,
requests: Sequence[DiffusionKVRequest],
computed_blocks: Sequence[KVCacheBlocks],
cached_prefix_len: int,
) -> int:
"""Return the minimum physical capacity for one public request.
Cached CFG rows may point to the same physical blocks. Count those hit
blocks once, then ask the native coordinator how many additional blocks
each row needs for its suffix. This gives an admission bound that still
works for warm requests and does not over-count shared prefix pages.
"""
coordinator = self.native_manager.coordinator
empty_blocks = self.native_manager.empty_kv_cache_blocks.blocks
return sum(
coordinator.get_num_blocks_to_allocate(
unique_hit_blocks = len(self._unique_blocks(computed_blocks))
suffix_blocks = 0
for request, hit in zip(requests, computed_blocks, strict=True):
native_required = coordinator.get_num_blocks_to_allocate(
request_id=request.request_id,
num_tokens=request.num_tokens,
new_computed_blocks=empty_blocks,
new_computed_blocks=hit.blocks,
num_encoder_tokens=0,
total_computed_tokens=0,
num_local_computed_tokens=0,
total_computed_tokens=cached_prefix_len,
num_local_computed_tokens=cached_prefix_len,
num_tokens_main_model=request.num_tokens,
apply_admission_cap=True,
)
for request in requests
)
evictable_hits = sum(block.ref_cnt == 0 and not block.is_null for group in hit.blocks for block in group)
suffix_blocks += max(native_required - evictable_hits, 0)
# ``get_num_blocks_to_allocate`` includes evictable hit pages because
# the live free-capacity check must first remove them from the free
# queue. They are already part of the pool's physical capacity, so do
# not count them a second time in this empty-pool bound.
return unique_hit_blocks + suffix_blocks
def has_request(self, public_request_id: str) -> bool:
return public_request_id in self._requests
@@ -123,7 +186,25 @@ class DiffusionKVCacheManager:
f"seq_len={request.seq_len}, max_model_len={self.max_model_len}"
)
required_blocks = self._get_empty_pool_required_blocks(requests)
self._prepare_block_hashes(requests)
hit_results = [self.native_manager.get_computed_blocks(request) for request in requests]
cached_prefix_len = min(result[1] for result in hit_results)
computed_blocks = [
self._clip_computed_blocks(
result[0],
hit_len=result[1],
cached_prefix_len=cached_prefix_len,
)
for result in hit_results
]
for request, result in zip(requests, hit_results, strict=True):
request.shared_prefix_boundary = result[2]
required_blocks = self._get_empty_pool_required_blocks(
requests,
computed_blocks,
cached_prefix_len,
)
if required_blocks > self._empty_pool_num_free_blocks:
raise DiffusionKVAdmissionError(
f"Diffusion KV request {public_request_id!r} cannot fit even when the block pool is empty: "
@@ -133,11 +214,20 @@ class DiffusionKVCacheManager:
allocated: list[DiffusionKVRequest] = []
sequence_metadata: list[DiffusionKVSequenceMetadata] = []
pinned_blocks = self._unique_blocks(computed_blocks)
if pinned_blocks:
# All CFG lookups happen before allocation so they can share one
# execution boundary. Pin every distinct hit until each native
# allocation has taken its own reference; otherwise an earlier
# branch's miss allocation could evict a later branch's hit.
self.native_manager.block_pool.touch(pinned_blocks)
try:
for request in requests:
for request, request_computed_blocks in zip(requests, computed_blocks, strict=True):
blocks = self.native_manager.allocate_slots(
request,
num_new_tokens=request.seq_len,
num_new_tokens=request.seq_len - cached_prefix_len,
num_new_computed_tokens=cached_prefix_len,
new_computed_blocks=request_computed_blocks,
delay_cache_blocks=True,
full_sequence_must_fit=True,
)
@@ -146,18 +236,23 @@ class DiffusionKVCacheManager:
allocated.clear()
return None
allocated.append(request)
request.num_computed_tokens = cached_prefix_len
sequence_metadata.append(
DiffusionKVSequenceMetadata(
sequence_id=request.sequence_id,
prefix_len=request.prefix_len,
target_len=request.target_len,
seq_len=request.seq_len,
block_ids=blocks.get_block_ids(),
block_ids=self.native_manager.get_block_ids(request.request_id),
cached_prefix_len=cached_prefix_len,
)
)
except Exception:
self._rollback(allocated)
raise
finally:
if pinned_blocks:
self.native_manager.block_pool.free_blocks(reversed(pinned_blocks))
metadata = DiffusionKVMetadata(
request_id=public_request_id,
@@ -170,6 +265,23 @@ class DiffusionKVCacheManager:
self._internal_request_ids.update(internal_ids)
return metadata
def publish_request(self, public_request_id: str) -> None:
"""Publish successfully materialized, block-aligned stable prefixes."""
if not self.enable_prefix_caching:
return
try:
requests = self._requests[public_request_id]
except KeyError as exc:
raise KeyError(f"Diffusion KV request {public_request_id!r} is not allocated") from exc
for request in requests:
publishable_prefix_len = min(
request.prefix_len // self.hash_block_size * self.hash_block_size,
len(request.block_hashes) * self.hash_block_size,
)
if publishable_prefix_len > 0:
self.native_manager.cache_blocks(request, publishable_prefix_len)
def get_metadata(self, public_request_id: str) -> DiffusionKVMetadata:
try:
return self._metadata[public_request_id]
@@ -181,6 +293,8 @@ class DiffusionKVCacheManager:
self._metadata.pop(public_request_id, None)
for request in reversed(requests):
free_kv_blocks_in_physical_order(self.native_manager, request)
request.num_computed_tokens = 0
request.shared_prefix_boundary = 0
self._internal_request_ids.discard(request.request_id)
def close(self) -> None:
@@ -190,3 +304,5 @@ class DiffusionKVCacheManager:
def _rollback(self, requests: Sequence[DiffusionKVRequest]) -> None:
for request in reversed(requests):
free_kv_blocks_in_physical_order(self.native_manager, request)
request.num_computed_tokens = 0
request.shared_prefix_boundary = 0
+2 -1
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Scheduler-to-Worker allocation payloads for diffusion KV.
@@ -40,6 +40,7 @@ class DiffusionKVSequenceMetadata:
target_len: int
seq_len: int
block_ids: tuple[list[int], ...]
cached_prefix_len: int = 0
context_ids: tuple[str, ...] = ()
num_computed_tokens: int = 0
@@ -4,7 +4,7 @@
from __future__ import annotations
import copy
from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any
@@ -154,6 +154,7 @@ class DiffusionKVModelRunnerBackend:
if attention_geometry is not None:
num_heads, num_kv_heads, head_size = attention_geometry
assert callable(set_attention_geometry)
try:
set_attention_geometry(
num_heads=num_heads,
@@ -396,6 +397,15 @@ class DiffusionKVModelRunnerBackend:
for sequence in metadata.sequences:
if type(sequence.sequence_id) is not int or sequence.sequence_id < 0:
raise ValueError(f"Diffusion KV sequence_id must be a non-negative integer: {sequence.sequence_id!r}")
if (
type(sequence.cached_prefix_len) is not int
or sequence.cached_prefix_len < 0
or sequence.cached_prefix_len > sequence.prefix_len
):
raise ValueError(
"Diffusion KV cached_prefix_len must lie within the stable prefix: "
f"cached={sequence.cached_prefix_len!r}, prefix={sequence.prefix_len!r}"
)
sequence_identity = (metadata.request_id, sequence.sequence_id, None)
sequence_install = self._validate_row(
identity=sequence_identity,
@@ -410,6 +420,7 @@ class DiffusionKVModelRunnerBackend:
sequence.prefix_len,
sequence.target_len,
sequence.seq_len,
sequence.cached_prefix_len,
sequence_install.block_ids,
sequence.context_ids,
)
@@ -593,7 +604,7 @@ class DiffusionKVModelRunnerBackend:
)
raise RuntimeError(f"Diffusion KV request state is missing logical length for {identity!r}")
def remove_diffusion_kv_requests(self, request_ids: list[str | tuple[str, int]]) -> int:
def remove_diffusion_kv_requests(self, request_ids: Sequence[str | tuple[str, int]]) -> int:
"""Retire Worker rows without logically freeing Scheduler-owned blocks."""
if (
not is_scheduler_paged_kv_mode(
@@ -84,14 +84,18 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
attention_config = vllm_config.attention_config
previous_backend = attention_config.backend
previous_backend_per_kind = attention_config.backend_per_kind
previous_pcp_size = vllm_config.parallel_config.prefill_context_parallel_size
parallel_config = vllm_config.parallel_config
previous_pcp_size = parallel_config.prefill_context_parallel_size
try:
# This is a portable vLLM backend request. The active platform
# resolves it to its native implementation, such as FlashAttention
# on CUDA or AscendAttentionBackend on NPU.
attention_config.backend = AttentionBackendEnum.FLASH_ATTN
attention_config.backend_per_kind = {}
vllm_config.parallel_config.prefill_context_parallel_size = 1
# Diffusion maps SP to PCP for MoE, not for attention. Omni's
# Ulysses hooks gather the sequence and shard heads before this
# native kernel, so backend selection must not request PCP.
parallel_config.prefill_context_parallel_size = 1
with set_current_vllm_config(vllm_config):
attn_backend = get_attn_backend(
head_size=spec.head_size,
@@ -103,7 +107,7 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
finally:
attention_config.backend = previous_backend
attention_config.backend_per_kind = previous_backend_per_kind
vllm_config.parallel_config.prefill_context_parallel_size = previous_pcp_size
parallel_config.prefill_context_parallel_size = previous_pcp_size
attn_backend = current_omni_platform.get_diffusion_paged_kv_attn_backend(
attn_backend,
ulysses_degree=ulysses_degree,
+93 -5
View File
@@ -1,13 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Any
from vllm.v1.core.kv_cache_utils import BlockHash
from vllm.multimodal.inputs import MultiModalFeatureSpec
from vllm.v1.core.kv_cache_utils import BlockHash, generate_block_hash_extra_keys, hash_block_tokens
from vllm.v1.request import RequestStatus
@@ -51,8 +52,10 @@ class DiffusionKVRequest:
An empty ``block_hashes`` sequence means the prefix has no canonical cache
identity yet. Such a request may use native request-local page allocation,
but consumers must not publish it through ``KVCacheManager.cache_blocks``.
Prefix publication becomes valid only after model preprocessing supplies
one canonical hash for every cacheable full block.
With prefix caching enabled, Engine invokes model input preparation to
attach token IDs and native multimodal identities / positions. The Manager
builds one canonical hash for every cacheable full block before lookup;
publication requires those hashes and successfully materialized KV.
"""
def __init__(
@@ -64,6 +67,9 @@ class DiffusionKVRequest:
target_len: int,
seq_len: int,
block_hashes: Sequence[BlockHash] = (),
cache_token_ids: Sequence[int] = (),
mm_features: Sequence[MultiModalFeatureSpec] = (),
cache_namespace: str = "diffusion-primary-v1",
kv_contexts: Sequence[DiffusionKVContext] = (),
kv_transfer_params: dict[str, Any] | None = None,
prompt_token_ids: list[int] | None = None,
@@ -83,6 +89,15 @@ class DiffusionKVRequest:
"prefix_len + target_len must not exceed seq_len: "
f"prefix_len={prefix_len}, target_len={target_len}, seq_len={seq_len}"
)
token_ids = tuple(cache_token_ids)
if token_ids and len(token_ids) < prefix_len:
raise ValueError(
f"cache_token_ids must cover the reusable prefix: tokens={len(token_ids)}, prefix_len={prefix_len}"
)
if any(type(token_id) is not int for token_id in token_ids):
raise TypeError("cache_token_ids must contain only integers")
if not isinstance(cache_namespace, str) or not cache_namespace:
raise ValueError("cache_namespace must be a non-empty string")
contexts = tuple(kv_contexts)
if any(not isinstance(context, DiffusionKVContext) for context in contexts):
@@ -96,6 +111,15 @@ class DiffusionKVRequest:
self.prefix_len = prefix_len
self.target_len = target_len
self.kv_contexts = contexts
self.cache_token_ids = token_ids
self.mm_features = list(mm_features)
self.cache_namespace = cache_namespace
# Native generate_block_hash_extra_keys consumes this Request surface.
# Diffusion adapter ID + scale are already isolated in cache_namespace;
# there are no separate LoRA-name, salt, or prompt-embedding inputs.
self.lora_request = None
self.cache_salt = None
self.prompt_embeds = None
# Native vLLM Request surface. Keep this list intentionally small and
# cover it with real-KVCacheManager conformance tests.
@@ -115,6 +139,70 @@ class DiffusionKVRequest:
self.kv_transfer_params = kv_transfer_params
self.prompt_token_ids = prompt_token_ids
def _validate_mm_features(self) -> None:
# Native block traversal expects sorted, non-overlapping placeholders.
# Do not silently accept nested/overlapping ranges: the traversal can
# skip a feature and incorrectly reuse KV. Combine such dependencies
# in the feature identifier instead.
previous_end = 0
for feature in self.mm_features:
if not isinstance(feature, MultiModalFeatureSpec):
raise TypeError("mm_features must contain native MultiModalFeatureSpec values")
position = feature.mm_position
end = position.offset + position.length
if position.offset < previous_end or position.length <= 0 or end > self.seq_len:
raise ValueError("mm_features must have valid sorted, non-overlapping positions within seq_len")
if not isinstance(feature.identifier, str) or not feature.identifier:
raise ValueError("mm_features must have non-empty content identifiers")
previous_end = end
def build_block_hashes(
self,
hash_block_size: int,
hash_function: Callable[[object], bytes],
) -> None:
"""Build native chained hashes for complete reusable-prefix blocks."""
if self.block_hashes:
self.skip_reading_prefix_cache = False
return
if hash_block_size <= 0:
raise ValueError(f"hash_block_size must be positive, got {hash_block_size}")
num_cache_tokens = self.prefix_len // hash_block_size * hash_block_size
if num_cache_tokens == 0 or not self.cache_token_ids:
return
if len(self.cache_token_ids) < num_cache_tokens:
raise ValueError(
"cache_token_ids do not cover the block-aligned reusable prefix: "
f"tokens={len(self.cache_token_ids)}, required={num_cache_tokens}"
)
self._validate_mm_features()
# Keep the first hash deterministic without mutating vLLM's process-wide
# ``NONE_HASH``. A diffusion manager can be created more than once in
# one process (for example in tests or worker reinitialization); using
# the global random root would make requests built by different manager
# instances impossible to match.
parent_hash: BlockHash | None = BlockHash(hash_function(("diffusion-prefix-root-v1", self.cache_namespace)))
hashes: list[BlockHash] = []
mm_index = 0
for start in range(0, num_cache_tokens, hash_block_size):
end = start + hash_block_size
# Reuse upstream MM intersection/offset logic directly. Keep our
# stable-prefix bound: native get_request_block_hasher hashes the
# entire token axis, including diffusion's mutable target region.
extra_keys, mm_index = generate_block_hash_extra_keys(self, start, end, mm_index)
parent_hash = hash_block_tokens(
hash_function=hash_function,
parent_block_hash=parent_hash,
curr_block_token_ids=self.cache_token_ids[start:end],
extra_keys=extra_keys,
)
hashes.append(parent_hash)
self.block_hashes = hashes
self.skip_reading_prefix_cache = not hashes
@property
def seq_len(self) -> int:
"""Complete first-step sequence length and allocation boundary."""