[Rust TreeCore] Harden runtime and CI parity (#37303)
This commit is contained in:
@@ -53,7 +53,7 @@ runs:
|
||||
path: |
|
||||
python/sglang/srt/rust_extensions/_*.so
|
||||
python/sglang/srt/mem_cache/rust_tree_core/mem_cache*.so
|
||||
key: ${{ inputs.cache_key_prefix }}-${{ hashFiles('rust/**', 'python/setup.py', 'python/pyproject.toml', 'python/sglang/srt/rust_extensions/torch_build.py', '.github/workflows/_pr-test-rust-ext-build.yml', 'scripts/ci/utils/stage_rust_ext_modules.sh') }}
|
||||
key: ${{ inputs.cache_key_prefix }}-${{ hashFiles('rust/**', 'proto/**', 'python/setup.py', 'python/pyproject.toml', 'python/sglang/srt/rust_extensions/torch_build.py', '.github/workflows/_pr-test-rust-ext-build.yml', 'scripts/ci/utils/stage_rust_ext_modules.sh') }}
|
||||
|
||||
# Job-wide, but only setup.py reads it, and only while building.
|
||||
# Whether the modules suit this interpreter is not decided here:
|
||||
|
||||
@@ -88,10 +88,12 @@ jobs:
|
||||
ref: ${{ inputs.git_ref || github.sha }}
|
||||
# Just what the cache key hashes, plus the action and script this job
|
||||
# runs: the workspace is cold here and the rest of the tree is mostly
|
||||
# docs. Both jobs must hash the same rust/** set, which this preserves.
|
||||
# docs. Both jobs must hash the same Rust extension inputs, which this
|
||||
# preserves.
|
||||
# Cone mode off is what allows naming a single file.
|
||||
sparse-checkout: |
|
||||
rust
|
||||
proto
|
||||
python/setup.py
|
||||
python/pyproject.toml
|
||||
python/sglang/srt/rust_extensions/torch_build.py
|
||||
@@ -115,7 +117,7 @@ jobs:
|
||||
path: |
|
||||
python/sglang/srt/rust_extensions/_*.so
|
||||
python/sglang/srt/mem_cache/rust_tree_core/mem_cache*.so
|
||||
key: ${{ inputs.cache_key_prefix }}-${{ hashFiles('rust/**', 'python/setup.py', 'python/pyproject.toml', 'python/sglang/srt/rust_extensions/torch_build.py', '.github/workflows/_pr-test-rust-ext-build.yml', 'scripts/ci/utils/stage_rust_ext_modules.sh') }}
|
||||
key: ${{ inputs.cache_key_prefix }}-${{ hashFiles('rust/**', 'proto/**', 'python/setup.py', 'python/pyproject.toml', 'python/sglang/srt/rust_extensions/torch_build.py', '.github/workflows/_pr-test-rust-ext-build.yml', 'scripts/ci/utils/stage_rust_ext_modules.sh') }}
|
||||
|
||||
# On a miss: different hash = rust/setup.py moved; no entries = evicted.
|
||||
- name: Report cache lookup
|
||||
@@ -324,6 +326,7 @@ jobs:
|
||||
# both have to check out the same set for hashFiles to agree.
|
||||
sparse-checkout: |
|
||||
rust
|
||||
proto
|
||||
python/setup.py
|
||||
python/pyproject.toml
|
||||
python/sglang/srt/rust_extensions/torch_build.py
|
||||
@@ -359,7 +362,7 @@ jobs:
|
||||
path: |
|
||||
python/sglang/srt/rust_extensions/_*.so
|
||||
python/sglang/srt/mem_cache/rust_tree_core/mem_cache*.so
|
||||
key: ${{ inputs.cache_key_prefix }}-${{ hashFiles('rust/**', 'python/setup.py', 'python/pyproject.toml', 'python/sglang/srt/rust_extensions/torch_build.py', '.github/workflows/_pr-test-rust-ext-build.yml', 'scripts/ci/utils/stage_rust_ext_modules.sh') }}
|
||||
key: ${{ inputs.cache_key_prefix }}-${{ hashFiles('rust/**', 'proto/**', 'python/setup.py', 'python/pyproject.toml', 'python/sglang/srt/rust_extensions/torch_build.py', '.github/workflows/_pr-test-rust-ext-build.yml', 'scripts/ci/utils/stage_rust_ext_modules.sh') }}
|
||||
|
||||
- name: Upload extension modules
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'rust/**'
|
||||
- 'proto/**'
|
||||
- 'python/setup.py'
|
||||
- 'python/pyproject.toml'
|
||||
- 'python/sglang/srt/rust_extensions/torch_build.py'
|
||||
|
||||
@@ -55,6 +55,7 @@ class _CrateSpec:
|
||||
manifest: Path
|
||||
workspace: Path
|
||||
features: tuple[str, ...]
|
||||
source_inputs: tuple[Path, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -80,7 +81,8 @@ def load_rust_extension(
|
||||
The crate is discovered from the workspace under ``rust/``: the one whose
|
||||
Cargo manifest declares ``[package.metadata.sglang] python-module`` equal
|
||||
to ``python_module`` (the same metadata setup.py uses for wheel builds), so
|
||||
new crates need no registration here.
|
||||
new crates need no registration here. Crates may declare ``source-inputs``
|
||||
relative to their manifest for build inputs outside the Rust workspace.
|
||||
|
||||
``auto`` prefers a module bundled in an installed wheel. In a source tree,
|
||||
it ignores unverified in-package artifacts and uses the fingerprinted cache
|
||||
@@ -152,9 +154,12 @@ def load_rust_extension(
|
||||
features=features,
|
||||
build_environment=build_environment,
|
||||
)
|
||||
if _source_digest(crate.workspace) != context.source_digest:
|
||||
if (
|
||||
_source_digest(crate.workspace, crate.source_inputs)
|
||||
!= context.source_digest
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Rust sources under {crate.workspace} changed during the build; "
|
||||
f"Rust extension sources for {crate.package} changed during the build; "
|
||||
"the result was not cached"
|
||||
)
|
||||
_stage_atomically(artifact, extension_path)
|
||||
@@ -216,6 +221,10 @@ def _discover_crate(workspace: Path, python_module: str) -> _CrateSpec:
|
||||
manifest=manifest,
|
||||
workspace=crate_workspace,
|
||||
features=tuple(sglang_metadata.get("features", ())),
|
||||
source_inputs=tuple(
|
||||
(manifest.parent / path).resolve()
|
||||
for path in sglang_metadata.get("source-inputs", ())
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -245,7 +254,7 @@ def _build_context(
|
||||
features = crate.features
|
||||
if extension_module is None:
|
||||
extension_module = crate.python_module
|
||||
source_digest = _source_digest(crate.workspace)
|
||||
source_digest = _source_digest(crate.workspace, crate.source_inputs)
|
||||
toolchain = {
|
||||
"cargo": _command_version(
|
||||
"cargo", "--version", "--verbose", cwd=crate.workspace
|
||||
@@ -289,10 +298,16 @@ def _build_context(
|
||||
)
|
||||
|
||||
|
||||
def _source_digest(workspace: Path) -> str:
|
||||
def _source_digest(workspace: Path, source_inputs: tuple[Path, ...] = ()) -> str:
|
||||
digest = hashlib.sha256()
|
||||
for path in _source_files(workspace):
|
||||
relative_path = path.relative_to(workspace).as_posix().encode()
|
||||
paths = set(_source_files(workspace))
|
||||
for source_input in source_inputs:
|
||||
if source_input.is_dir():
|
||||
paths.update(_source_files(source_input))
|
||||
else:
|
||||
paths.add(source_input)
|
||||
for path in sorted(paths, key=lambda item: os.path.relpath(item, workspace)):
|
||||
relative_path = Path(os.path.relpath(path, workspace)).as_posix().encode()
|
||||
digest.update(len(relative_path).to_bytes(8, "big"))
|
||||
digest.update(relative_path)
|
||||
if path.is_symlink():
|
||||
|
||||
@@ -9,6 +9,9 @@ license.workspace = true
|
||||
# of the main sglang wheel at the given import path.
|
||||
[package.metadata.sglang]
|
||||
python-module = "sglang.srt.rust_extensions._grpc"
|
||||
# build.rs compiles schemas from outside the Rust workspace; include them in
|
||||
# the source-loader artifact fingerprint.
|
||||
source-inputs = ["../../proto"]
|
||||
# Always build optimized, even for an editable install.
|
||||
debug = false
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ impl<K: ChildKeyType> TreeComponent<K> for FullComponent {
|
||||
&self,
|
||||
tree_core: &UnifiedTreeCore<K>,
|
||||
mut result: MatchResult,
|
||||
last_device_node_idx: NodeIdx_,
|
||||
best_match_node_idx: NodeIdx_,
|
||||
params: &MatchPrefixParams<'_, K>,
|
||||
value_chunks: &[Tensor],
|
||||
best_value_len: usize,
|
||||
@@ -56,9 +58,8 @@ impl<K: ChildKeyType> TreeComponent<K> for FullComponent {
|
||||
// Compute Full KV host hit length: walk from last_host_node up to
|
||||
// last_device_node, summing host_value lengths of evicted nodes.
|
||||
let mut kv_host_hit = 0;
|
||||
let mut node_idx = tree_core.arena.resolve(result.best_match_node_id);
|
||||
let last_device_idx = tree_core.arena.resolve(result.last_device_node_id);
|
||||
while node_idx != last_device_idx {
|
||||
let mut node_idx = best_match_node_idx;
|
||||
while node_idx != last_device_node_idx {
|
||||
let node = tree_core.arena.node(node_idx);
|
||||
let parent = node.try_parent().unwrap_or_else(|| {
|
||||
panic!(
|
||||
@@ -469,7 +470,10 @@ impl<K: ChildKeyType> TreeComponent<K> for FullComponent {
|
||||
{
|
||||
let mut offset = 0i64;
|
||||
for &loaded_id in transfer.nodes_to_load.iter().flatten() {
|
||||
let loaded_idx = tree_core.arena.resolve(loaded_id);
|
||||
let loaded_idx = tree_core
|
||||
.arena
|
||||
.resolve(loaded_id)
|
||||
.expect("load-back transfers must reference live nodes");
|
||||
let loaded = tree_core.arena.node_mut(loaded_idx);
|
||||
let n_len = loaded.host_value_len(FULL) as i64;
|
||||
loaded
|
||||
|
||||
@@ -127,6 +127,8 @@ impl<K: ChildKeyType> TreeComponent<K> for MambaComponent {
|
||||
&self,
|
||||
tree_core: &UnifiedTreeCore<K>,
|
||||
mut result: MatchResult,
|
||||
_last_device_node_idx: NodeIdx_,
|
||||
best_match_node_idx: NodeIdx_,
|
||||
_params: &MatchPrefixParams<'_, K>,
|
||||
_value_chunks: &[Tensor],
|
||||
_best_value_len: usize,
|
||||
@@ -143,9 +145,7 @@ impl<K: ChildKeyType> TreeComponent<K> for MambaComponent {
|
||||
|
||||
// HiCache: if mamba was evicted from device but has host backup,
|
||||
// ensure mamba_host_hit_length >= 1 so load_back is triggered.
|
||||
let last_node = tree_core
|
||||
.arena
|
||||
.node(tree_core.arena.resolve(result.best_match_node_id));
|
||||
let last_node = tree_core.arena.node(best_match_node_idx);
|
||||
if !last_node.has_device_value(MAMBA) && last_node.has_host_value(MAMBA) {
|
||||
result.mamba_host_hit_length = result.mamba_host_hit_length.max(1);
|
||||
}
|
||||
@@ -643,7 +643,12 @@ impl<K: ChildKeyType> TreeComponent<K> for MambaComponent {
|
||||
let target_node_id = insert_result
|
||||
.as_deref()
|
||||
.and_then(|result| result.inserted_host_node)
|
||||
.map(|id| tree_core.arena.resolve(id));
|
||||
.map(|id| {
|
||||
tree_core
|
||||
.arena
|
||||
.resolve(id)
|
||||
.expect("prefetch insert results must reference live nodes")
|
||||
});
|
||||
let attach_target = match (host_indices, target_node_id) {
|
||||
(Some(_), Some(target))
|
||||
if loaded && !tree_core.arena.has_host_value(target, MAMBA) =>
|
||||
|
||||
@@ -130,6 +130,8 @@ pub trait TreeComponent<K: ChildKeyType> {
|
||||
&self,
|
||||
tree_core: &UnifiedTreeCore<K>,
|
||||
result: MatchResult,
|
||||
_last_device_node_idx: NodeIdx_,
|
||||
_best_match_node_idx: NodeIdx_,
|
||||
params: &MatchPrefixParams<'_, K>,
|
||||
value_chunks: &[Tensor],
|
||||
best_value_len: usize,
|
||||
|
||||
@@ -225,7 +225,12 @@ impl SwaComponent {
|
||||
});
|
||||
let target = insert_result
|
||||
.and_then(|result| result.inserted_host_node)
|
||||
.map(|id| tree_core.arena.resolve(id));
|
||||
.map(|id| {
|
||||
tree_core
|
||||
.arena
|
||||
.resolve(id)
|
||||
.expect("prefetch insert results must reference live nodes")
|
||||
});
|
||||
|
||||
let (Some(target), Some(host_indices)) = (target, transfer.host_indices.as_ref()) else {
|
||||
if let Some(host_indices) = &transfer.host_indices {
|
||||
@@ -233,6 +238,15 @@ impl SwaComponent {
|
||||
}
|
||||
return;
|
||||
};
|
||||
// Cache-mode graft commit only (buffer fills never reach here):
|
||||
// a hit-shrunk window mid-tree is missing its head, so drop it.
|
||||
// Root anchors are complete windows of their own.
|
||||
if node_id != tree_core.arena.root()
|
||||
&& window_require_pages < self.sliding_window_size.div_ceil(page_size)
|
||||
{
|
||||
self.release_swa_host_(host_indices.shallow_clone(), cache_actions);
|
||||
return;
|
||||
}
|
||||
if window_require_pages == 0 || loaded_pages < window_require_pages {
|
||||
self.release_swa_host_(host_indices.shallow_clone(), cache_actions);
|
||||
return;
|
||||
@@ -338,6 +352,8 @@ impl<K: ChildKeyType> TreeComponent<K> for SwaComponent {
|
||||
&self,
|
||||
tree_core: &UnifiedTreeCore<K>,
|
||||
mut result: MatchResult,
|
||||
_last_device_node_idx: NodeIdx_,
|
||||
best_match_node_idx: NodeIdx_,
|
||||
params: &MatchPrefixParams<'_, K>,
|
||||
value_chunks: &[Tensor],
|
||||
best_value_len: usize,
|
||||
@@ -347,9 +363,7 @@ impl<K: ChildKeyType> TreeComponent<K> for SwaComponent {
|
||||
// toward the SWA host hit.
|
||||
let mut n_swa = 0;
|
||||
let mut swa_host_hit = 0;
|
||||
let mut node = tree_core
|
||||
.arena
|
||||
.node(tree_core.arena.resolve(result.best_match_node_id));
|
||||
let mut node = tree_core.arena.node(best_match_node_idx);
|
||||
while !node.is_root() && n_swa < self.sliding_window_size {
|
||||
if node.has_device_value(SWA) {
|
||||
n_swa += node.device_value_len(SWA);
|
||||
@@ -965,7 +979,10 @@ impl<K: ChildKeyType> TreeComponent<K> for SwaComponent {
|
||||
let mut swa_chunks: Vec<Tensor> = Vec::new();
|
||||
let mut offset = 0i64;
|
||||
for &loaded_id in transfer.nodes_to_load.iter().flatten() {
|
||||
let loaded_idx = tree_core.arena.resolve(loaded_id);
|
||||
let loaded_idx = tree_core
|
||||
.arena
|
||||
.resolve(loaded_id)
|
||||
.expect("load-back transfers must reference live nodes");
|
||||
let n_tokens = tree_core.arena.host_value_len(loaded_idx, SWA) as i64;
|
||||
let swa_chunk = device_indices.narrow(0, offset, n_tokens).copy();
|
||||
tree_core.set_component_device_value_(
|
||||
|
||||
@@ -688,14 +688,21 @@ pub struct ValueState {
|
||||
|
||||
// Tree-core runtime errors.
|
||||
|
||||
/// A public node handle does not name a live arena node.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
|
||||
#[error("node {node_id} is not allocated")]
|
||||
pub struct NodeAccessError {
|
||||
pub node_id: NodeId,
|
||||
}
|
||||
|
||||
/// Errors surfaced from the tree-core runtime API when a caller violates a documented
|
||||
/// contract (freeing an unallocated node, allocating under a freed parent).
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TreeCoreRuntimeError {
|
||||
/// A public NodeId no longer names a live arena node.
|
||||
#[error("node {node_id} is not allocated")]
|
||||
NodeNotAllocated { node_id: NodeId },
|
||||
#[error(transparent)]
|
||||
NodeAccess(#[from] NodeAccessError),
|
||||
/// `begin_insert`/`insert` called while a resumable insert is suspended.
|
||||
#[error("concurrent insert walks")]
|
||||
ConcurrentInsertWalk,
|
||||
@@ -736,6 +743,10 @@ pub enum TreeCoreRuntimeError {
|
||||
/// A host insert below a non-root anchor must remain in that anchor's namespace.
|
||||
#[error("insert_host namespace does not match non-root anchor {node_id}")]
|
||||
InsertHostNamespaceMismatch { node_id: NodeId },
|
||||
/// An inspection-only invariant check failed without mutating the tree.
|
||||
#[cfg(any(test, feature = "inspection"))]
|
||||
#[error("{0}")]
|
||||
InspectionAssertion(String),
|
||||
}
|
||||
|
||||
// Unigram and bigram child keys.
|
||||
@@ -1058,18 +1069,12 @@ impl<K: ChildKeyType> NodeArena<K> {
|
||||
self.root = self.alloc_root();
|
||||
}
|
||||
|
||||
/// The live slot for an external handle; panics on a freed or unknown id.
|
||||
#[track_caller]
|
||||
pub fn resolve(&self, id: NodeId) -> NodeIdx_ {
|
||||
*self
|
||||
.id_map
|
||||
/// The live slot for an external handle.
|
||||
pub fn resolve(&self, id: NodeId) -> Result<NodeIdx_, NodeAccessError> {
|
||||
self.id_map
|
||||
.get(&id)
|
||||
.unwrap_or_else(|| panic!("node {id} is not allocated"))
|
||||
}
|
||||
|
||||
/// The live slot for an external handle, or None if freed/unknown.
|
||||
pub fn try_resolve(&self, id: NodeId) -> Option<NodeIdx_> {
|
||||
self.id_map.get(&id).copied()
|
||||
.copied()
|
||||
.ok_or(NodeAccessError { node_id: id })
|
||||
}
|
||||
|
||||
/// Mint the next external handle for the slot and index it.
|
||||
|
||||
@@ -12,7 +12,7 @@ use tch::{Device, Kind, Tensor};
|
||||
|
||||
use crate::components::{ComponentType, FULL, MAMBA, SWA};
|
||||
use crate::node::ChildKeyType;
|
||||
use crate::node::{KeyNamespaceRef, NodeId, TreeCoreRuntimeError};
|
||||
use crate::node::{KeyNamespaceRef, NodeAccessError, NodeId, TreeCoreRuntimeError};
|
||||
use crate::unified_tree_core::KvCacheEvent;
|
||||
use crate::unified_tree_core::{
|
||||
BufferBackupSnapshot, BufferBackupState, CacheAction, CacheInitParams, CacheTransferPhase,
|
||||
@@ -65,17 +65,21 @@ fn parse_evict_layer(target: u8) -> PyResult<EvictLayer> {
|
||||
}
|
||||
}
|
||||
|
||||
fn node_access_error(error: NodeAccessError) -> PyErr {
|
||||
PyKeyError::new_err(error.node_id)
|
||||
}
|
||||
|
||||
/// Convert an expected tree-core contract failure without unwinding through PyO3.
|
||||
fn tree_core_runtime_error(error: TreeCoreRuntimeError) -> PyErr {
|
||||
match error {
|
||||
TreeCoreRuntimeError::NodeNotAllocated { node_id } => PyKeyError::new_err(node_id),
|
||||
TreeCoreRuntimeError::NodeAccess(error) => node_access_error(error),
|
||||
error => PyRuntimeError::new_err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tree_core_assertion_error(error: TreeCoreRuntimeError) -> PyErr {
|
||||
match error {
|
||||
TreeCoreRuntimeError::NodeNotAllocated { node_id } => PyKeyError::new_err(node_id),
|
||||
TreeCoreRuntimeError::NodeAccess(error) => node_access_error(error),
|
||||
error => PyAssertionError::new_err(error.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -1082,10 +1086,12 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
.into_iter()
|
||||
.map(parse_component_type)
|
||||
.collect::<PyResult<Vec<_>>>()?;
|
||||
let result = py.allow_threads(|| {
|
||||
self.core()
|
||||
.inc_lock_ref_with_skip(node_id, &skip_lock_components)
|
||||
});
|
||||
let result = py
|
||||
.allow_threads(|| {
|
||||
self.core()
|
||||
.inc_lock_ref_with_skip(node_id, &skip_lock_components)
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
Ok(IncLockRefResultBinding::from_result(result))
|
||||
}
|
||||
|
||||
@@ -1098,7 +1104,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
skip_swa: bool,
|
||||
) -> PyResult<()> {
|
||||
let params = params.map(|p| p.to_dec_lock_ref_params()).transpose()?;
|
||||
py.allow_threads(|| self.core().dec_lock_ref(node_id, params.as_ref(), skip_swa));
|
||||
py.allow_threads(|| self.core().dec_lock_ref(node_id, params.as_ref(), skip_swa))
|
||||
.map_err(node_access_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1116,18 +1123,20 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
.into_iter()
|
||||
.map(|(ct, node_ids)| Ok((parse_component_type(ct)?, node_ids)))
|
||||
.collect::<PyResult<HashMap<_, _>>>()?;
|
||||
let (device_frees, host_frees) = py.allow_threads(|| {
|
||||
let mut device_frees = HashMap::new();
|
||||
let mut host_frees = HashMap::new();
|
||||
self.core().dec_swa_lock_only_with_skip(
|
||||
node_id,
|
||||
swa_uuid_for_lock,
|
||||
Some(&skip_lock_node_ids),
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
(device_frees, host_frees)
|
||||
});
|
||||
let (device_frees, host_frees) = py
|
||||
.allow_threads(|| {
|
||||
let mut device_frees = HashMap::new();
|
||||
let mut host_frees = HashMap::new();
|
||||
self.core().dec_swa_lock_only_with_skip(
|
||||
node_id,
|
||||
swa_uuid_for_lock,
|
||||
Some(&skip_lock_node_ids),
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
)?;
|
||||
Ok((device_frees, host_frees))
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
Ok((frees_to_py(py, device_frees)?, frees_to_py(py, host_frees)?))
|
||||
}
|
||||
|
||||
@@ -1157,7 +1166,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.set_component_device_value(node_id, component_type, value)
|
||||
});
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1169,11 +1179,13 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
component_type: u8,
|
||||
) -> PyResult<Option<PyTensor>> {
|
||||
let component_type = parse_component_type(component_type)?;
|
||||
let value = py.allow_threads(|| {
|
||||
self.core()
|
||||
.get_component_device_value(node_id, component_type)
|
||||
.map(|tensor| tensor.shallow_clone())
|
||||
});
|
||||
let value = py
|
||||
.allow_threads(|| {
|
||||
self.core()
|
||||
.get_component_device_value(node_id, component_type)
|
||||
.map(|value| value.map(|tensor| tensor.shallow_clone()))
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
Ok(value.map(PyTensor))
|
||||
}
|
||||
|
||||
@@ -1221,11 +1233,13 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<EvictDeviceLeafResultBinding> {
|
||||
let (backup, result) = py.allow_threads(move || {
|
||||
let mut core = self.core();
|
||||
let is_write_back = core.is_write_back;
|
||||
core.evict_device_leaf(node_id, is_write_back)
|
||||
});
|
||||
let (backup, result) = py
|
||||
.allow_threads(move || {
|
||||
let mut core = self.core();
|
||||
let is_write_back = core.is_write_back;
|
||||
core.evict_device_leaf(node_id, is_write_back)
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
Ok(EvictDeviceLeafResultBinding {
|
||||
backup_kv: backup
|
||||
.map(|backup| cache_action_to_py(py, CacheAction::BackupKV(backup)))
|
||||
@@ -1264,11 +1278,14 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
from_node_id: NodeId,
|
||||
until_node_id: NodeId,
|
||||
) -> PyTensor {
|
||||
PyTensor(py.allow_threads(|| {
|
||||
self.core()
|
||||
.collect_full_device_indices(from_node_id, until_node_id)
|
||||
}))
|
||||
) -> PyResult<PyTensor> {
|
||||
let value = py
|
||||
.allow_threads(|| {
|
||||
self.core()
|
||||
.collect_full_device_indices(from_node_id, until_node_id)
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
Ok(PyTensor(value))
|
||||
}
|
||||
|
||||
/// Every FULL device value in the tree, concatenated.
|
||||
@@ -1337,8 +1354,9 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
}
|
||||
|
||||
/// Whether the node's FULL device value has been evicted.
|
||||
fn is_full_device_evicted(&self, py: Python<'_>, node_id: NodeId) -> bool {
|
||||
fn is_full_device_evicted(&self, py: Python<'_>, node_id: NodeId) -> PyResult<bool> {
|
||||
py.allow_threads(|| self.core().is_full_device_evicted(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// Mark the host tier (HiCache) as wired.
|
||||
@@ -1382,7 +1400,7 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
}
|
||||
let result = py
|
||||
.allow_threads(move || {
|
||||
self.core().try_insert_host_in_namespace(
|
||||
self.core().insert_host_in_namespace(
|
||||
node_id,
|
||||
KeyNamespaceRef::new(extra_key.as_deref(), cache_salt.as_deref()),
|
||||
key,
|
||||
@@ -1400,8 +1418,9 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<(PyTensor, Py<PyDict>)> {
|
||||
let (device_value, comp_xfers) =
|
||||
py.allow_threads(|| self.core().build_backup_spec(node_id));
|
||||
let (device_value, comp_xfers) = py
|
||||
.allow_threads(|| self.core().build_backup_spec(node_id))
|
||||
.map_err(node_access_error)?;
|
||||
Ok((PyTensor(device_value), comp_xfers_to_py(py, comp_xfers)?))
|
||||
}
|
||||
|
||||
@@ -1412,10 +1431,12 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
node_id: NodeId,
|
||||
pass_prefix_keys: bool,
|
||||
) -> PyResult<Option<StorageBackupSpecBinding>> {
|
||||
let spec = py.allow_threads(|| {
|
||||
self.core()
|
||||
.build_storage_backup_spec(node_id, pass_prefix_keys)
|
||||
});
|
||||
let spec = py
|
||||
.allow_threads(|| {
|
||||
self.core()
|
||||
.build_storage_backup_spec(node_id, pass_prefix_keys)
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
let Some(spec) = spec else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -1450,7 +1471,7 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
let host_indices = host_indices.map(|t| t.0);
|
||||
let transfers = py
|
||||
.allow_threads(|| {
|
||||
self.core().try_build_hicache_transfers(
|
||||
self.core().build_hicache_transfers(
|
||||
component_type,
|
||||
node_id,
|
||||
phase,
|
||||
@@ -1477,37 +1498,37 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<(Option<String>, Option<String>)> {
|
||||
py.allow_threads(|| self.core().try_prefetch_anchor_info(node_id))
|
||||
.map_err(tree_core_runtime_error)
|
||||
py.allow_threads(|| self.core().prefetch_anchor_info(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// Whether the node's Full KV is present on host.
|
||||
fn node_backuped(&self, py: Python<'_>, node_id: NodeId) -> PyResult<bool> {
|
||||
py.allow_threads(|| self.core().try_node_backuped(node_id))
|
||||
.map_err(tree_core_runtime_error)
|
||||
py.allow_threads(|| self.core().node_backuped(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// Whether the node is a (default or named) root.
|
||||
fn is_root(&self, py: Python<'_>, node_id: NodeId) -> PyResult<bool> {
|
||||
py.allow_threads(|| self.core().try_is_root(node_id))
|
||||
.map_err(tree_core_runtime_error)
|
||||
py.allow_threads(|| self.core().is_root(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// The node's last page hash, or None when it was never hashed.
|
||||
fn get_last_hash_value(&self, py: Python<'_>, node_id: NodeId) -> PyResult<Option<String>> {
|
||||
py.allow_threads(|| self.core().try_get_last_hash_value(node_id))
|
||||
.map_err(tree_core_runtime_error)
|
||||
py.allow_threads(|| self.core().get_last_hash_value(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// The hash chain of the node's ancestors, in root-to-parent order.
|
||||
fn get_prefix_hash_values(&self, py: Python<'_>, node_id: NodeId) -> PyResult<Vec<String>> {
|
||||
py.allow_threads(|| self.core().try_get_prefix_hash_values(node_id))
|
||||
.map_err(tree_core_runtime_error)
|
||||
py.allow_threads(|| self.core().get_prefix_hash_values(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn get_hash_values(&self, py: Python<'_>, node_id: NodeId) -> PyResult<Vec<String>> {
|
||||
py.allow_threads(|| self.core().try_get_hash_values(node_id))
|
||||
.map_err(tree_core_runtime_error)
|
||||
py.allow_threads(|| self.core().get_hash_values(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn snapshot_buffer_backup(
|
||||
@@ -1546,8 +1567,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
}
|
||||
|
||||
fn dfs_weight_order(&self, py: Python<'_>, node_ids: Vec<NodeId>) -> PyResult<Vec<usize>> {
|
||||
py.allow_threads(|| self.core().try_dfs_weight_order(&node_ids))
|
||||
.map_err(tree_core_runtime_error)
|
||||
py.allow_threads(|| self.core().dfs_weight_order(&node_ids))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// Commit each component's HiCache transfers; returns the new cache actions.
|
||||
@@ -1582,22 +1603,24 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let (cache_actions, mamba_exist) = py.allow_threads(move || {
|
||||
let mut cache_actions = Vec::new();
|
||||
let mut insert_result = insert_result;
|
||||
self.core().commit_hicache_transfers(
|
||||
node_id,
|
||||
phase,
|
||||
comp_xfers,
|
||||
&mut cache_actions,
|
||||
insert_result.as_mut(),
|
||||
pool_storage_result.as_ref(),
|
||||
);
|
||||
(
|
||||
cache_actions,
|
||||
insert_result.map(|result| result.mamba_exist),
|
||||
)
|
||||
});
|
||||
let (cache_actions, mamba_exist) = py
|
||||
.allow_threads(move || {
|
||||
let mut cache_actions = Vec::new();
|
||||
let mut insert_result = insert_result;
|
||||
self.core().commit_hicache_transfers(
|
||||
node_id,
|
||||
phase,
|
||||
comp_xfers,
|
||||
&mut cache_actions,
|
||||
insert_result.as_mut(),
|
||||
pool_storage_result.as_ref(),
|
||||
)?;
|
||||
Ok((
|
||||
cache_actions,
|
||||
insert_result.map(|result| result.mamba_exist),
|
||||
))
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
Ok((cache_actions_to_py(py, cache_actions)?, mamba_exist))
|
||||
}
|
||||
|
||||
@@ -1611,7 +1634,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
) -> PyResult<()> {
|
||||
let comp_xfers = comp_xfers_from_args(comp_xfers)?;
|
||||
let host_indices = host_indices.0;
|
||||
py.allow_threads(move || self.core().commit_backup(node_id, host_indices, comp_xfers));
|
||||
py.allow_threads(move || self.core().commit_backup(node_id, host_indices, comp_xfers))
|
||||
.map_err(node_access_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1626,7 +1650,7 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
mamba_pool_idx: mamba_pool_idx.map(|t| t.0),
|
||||
};
|
||||
let (kv_xfer, comp_xfers) = py
|
||||
.allow_threads(move || self.core().try_build_load_back_spec(node_id, Some(&req)))
|
||||
.allow_threads(move || self.core().build_load_back_spec(node_id, Some(&req)))
|
||||
.map_err(tree_core_assertion_error)?;
|
||||
Ok((
|
||||
transfer_to_py(py, kv_xfer)?,
|
||||
@@ -1646,17 +1670,19 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
let kv_xfer = transfer_from_args(kv_xfer)?;
|
||||
let comp_xfers = comp_xfers_from_args(comp_xfers)?;
|
||||
let device_indices = device_indices.0;
|
||||
let actions = py.allow_threads(move || {
|
||||
self.core()
|
||||
.commit_load_back(node_id, device_indices, kv_xfer, comp_xfers)
|
||||
});
|
||||
let actions = py
|
||||
.allow_threads(move || {
|
||||
self.core()
|
||||
.commit_load_back(node_id, device_indices, kv_xfer, comp_xfers)
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
cache_actions_to_py(py, actions)
|
||||
}
|
||||
|
||||
/// Release a node's device KV once its host copy exists.
|
||||
fn demote(&self, py: Python<'_>, node_id: NodeId) -> PyResult<DemoteResultBinding> {
|
||||
let result = py
|
||||
.allow_threads(move || self.core().try_demote(node_id))
|
||||
.allow_threads(move || self.core().demote(node_id))
|
||||
.map_err(tree_core_assertion_error)?;
|
||||
Ok(DemoteResultBinding {
|
||||
tracker: tracker_to_py(result.tracker),
|
||||
@@ -1688,7 +1714,9 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
tail_node_id: NodeId,
|
||||
) -> PyResult<HostEvictionResultBinding> {
|
||||
let result = py.allow_threads(move || self.core().evict_excess_path_states(tail_node_id));
|
||||
let result = py
|
||||
.allow_threads(move || self.core().evict_excess_path_states(tail_node_id))
|
||||
.map_err(node_access_error)?;
|
||||
Ok(HostEvictionResultBinding {
|
||||
tracker: tracker_to_py(result.tracker),
|
||||
new_device_frees: frees_to_py(py, result.device_frees)?,
|
||||
@@ -1697,9 +1725,15 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
}
|
||||
|
||||
/// Bump the reference count on a node's host-side component locks.
|
||||
fn inc_host_lock_ref(&self, py: Python<'_>, node_id: NodeId) -> IncLockRefResultBinding {
|
||||
let result = py.allow_threads(|| self.core().inc_host_lock_ref(node_id));
|
||||
IncLockRefResultBinding {
|
||||
fn inc_host_lock_ref(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<IncLockRefResultBinding> {
|
||||
let result = py
|
||||
.allow_threads(|| self.core().inc_host_lock_ref(node_id))
|
||||
.map_err(node_access_error)?;
|
||||
Ok(IncLockRefResultBinding {
|
||||
delta: result.delta,
|
||||
swa_uuid_for_lock: result.swa_uuid_for_lock,
|
||||
swa_uuid_for_host_lock: result.swa_uuid_for_host_lock,
|
||||
@@ -1708,7 +1742,7 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
.into_iter()
|
||||
.map(|(ct, node_ids)| (component_type_to_u8(ct), node_ids))
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrease the reference count on a node's host-side component locks.
|
||||
@@ -1719,7 +1753,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
params: Option<&DecLockRefParamsBinding>,
|
||||
) -> PyResult<()> {
|
||||
let params = params.map(|p| p.to_dec_lock_ref_params()).transpose()?;
|
||||
py.allow_threads(|| self.core().dec_host_lock_ref(node_id, params.as_ref()));
|
||||
py.allow_threads(|| self.core().dec_host_lock_ref(node_id, params.as_ref()))
|
||||
.map_err(node_access_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1811,7 +1846,9 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<DropSubtreeResultBinding> {
|
||||
let (dropped, result) = py.allow_threads(move || self.core().drop_subtree_no_host(node_id));
|
||||
let (dropped, result) = py
|
||||
.allow_threads(move || self.core().drop_subtree_no_host(node_id))
|
||||
.map_err(node_access_error)?;
|
||||
Ok(DropSubtreeResultBinding {
|
||||
dropped,
|
||||
tracker: tracker_to_py(result.tracker),
|
||||
@@ -1826,18 +1863,26 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_ids: Vec<NodeId>,
|
||||
ack_id: NodeId,
|
||||
) -> Vec<NodeId> {
|
||||
py.allow_threads(|| self.core().mark_write_through_pending(node_ids, ack_id))
|
||||
) -> PyResult<Vec<NodeId>> {
|
||||
py.allow_threads(move || self.core().mark_write_through_pending(node_ids, ack_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// Clear the write-through-pending mark on the acked nodes.
|
||||
fn finish_write_through(&self, py: Python<'_>, node_ids: Vec<NodeId>, ack_id: NodeId) {
|
||||
py.allow_threads(|| self.core().finish_write_through(node_ids, ack_id));
|
||||
fn finish_write_through(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_ids: Vec<NodeId>,
|
||||
ack_id: NodeId,
|
||||
) -> PyResult<()> {
|
||||
py.allow_threads(move || self.core().finish_write_through(node_ids, ack_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// Clear the in-flight H->D marks on the anchor's root path at ack time.
|
||||
fn finish_load_back(&self, py: Python<'_>, anchor_node_id: NodeId) {
|
||||
py.allow_threads(|| self.core().finish_load_back(anchor_node_id));
|
||||
fn finish_load_back(&self, py: Python<'_>, anchor_node_id: NodeId) -> PyResult<()> {
|
||||
py.allow_threads(|| self.core().finish_load_back(anchor_node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
/// Order-sensitive digest of reclaimed coexisting host values.
|
||||
@@ -1853,7 +1898,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
component_type: u8,
|
||||
) -> PyResult<bool> {
|
||||
let ct = parse_component_type(component_type)?;
|
||||
Ok(py.allow_threads(|| self.core().component_has_host_value_only(node_id, ct)))
|
||||
py.allow_threads(|| self.core().component_has_host_value_only(node_id, ct))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1863,24 +1909,33 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(|| self.core().inspect_contains_node(node_id))
|
||||
}
|
||||
|
||||
fn inspect_get_parent_node_id(&self, py: Python<'_>, node_id: NodeId) -> Option<NodeId> {
|
||||
fn inspect_get_parent_node_id(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<Option<NodeId>> {
|
||||
py.allow_threads(|| self.core().inspect_get_parent_node_id(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_child_node_ids(&self, py: Python<'_>, node_id: NodeId) -> Vec<NodeId> {
|
||||
fn inspect_get_child_node_ids(&self, py: Python<'_>, node_id: NodeId) -> PyResult<Vec<NodeId>> {
|
||||
py.allow_threads(|| self.core().inspect_get_child_node_ids(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_node_key_length(&self, py: Python<'_>, node_id: NodeId) -> usize {
|
||||
fn inspect_get_node_key_length(&self, py: Python<'_>, node_id: NodeId) -> PyResult<usize> {
|
||||
py.allow_threads(|| self.core().inspect_get_node_key_length(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_node_token_ids(&self, py: Python<'_>, node_id: NodeId) -> Vec<i64> {
|
||||
fn inspect_get_node_token_ids(&self, py: Python<'_>, node_id: NodeId) -> PyResult<Vec<i64>> {
|
||||
py.allow_threads(|| self.core().inspect_get_node_token_ids(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_is_node_key_bigram(&self, py: Python<'_>, node_id: NodeId) -> bool {
|
||||
fn inspect_is_node_key_bigram(&self, py: Python<'_>, node_id: NodeId) -> PyResult<bool> {
|
||||
py.allow_threads(|| self.core().inspect_is_node_key_bigram(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_component_host_value(
|
||||
@@ -1890,12 +1945,12 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
component_type: u8,
|
||||
) -> PyResult<Option<PyTensor>> {
|
||||
let component_type = parse_component_type(component_type)?;
|
||||
Ok(py
|
||||
.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_get_component_host_value(node_id, component_type)
|
||||
})
|
||||
.map(PyTensor))
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_get_component_host_value(node_id, component_type)
|
||||
})
|
||||
.map(|value| value.map(PyTensor))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_component_device_lock_ref(
|
||||
@@ -1905,22 +1960,25 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
component_type: u8,
|
||||
) -> PyResult<u32> {
|
||||
let component_type = parse_component_type(component_type)?;
|
||||
Ok(py.allow_threads(|| {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_get_component_device_lock_ref(node_id, component_type)
|
||||
}))
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_node_hit_count(&self, py: Python<'_>, node_id: NodeId) -> i64 {
|
||||
fn inspect_get_node_hit_count(&self, py: Python<'_>, node_id: NodeId) -> PyResult<i64> {
|
||||
py.allow_threads(|| self.core().inspect_get_node_hit_count(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_write_through_pending_id(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> Option<usize> {
|
||||
) -> PyResult<Option<usize>> {
|
||||
py.allow_threads(|| self.core().inspect_get_write_through_pending_id(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_is_node_in_device_lru(
|
||||
@@ -1930,10 +1988,11 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
component_type: u8,
|
||||
) -> PyResult<bool> {
|
||||
let component_type = parse_component_type(component_type)?;
|
||||
Ok(py.allow_threads(|| {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_is_node_in_device_lru(node_id, component_type)
|
||||
}))
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_is_node_in_host_lru(
|
||||
@@ -1943,10 +2002,11 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
component_type: u8,
|
||||
) -> PyResult<bool> {
|
||||
let component_type = parse_component_type(component_type)?;
|
||||
Ok(py.allow_threads(|| {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_is_node_in_host_lru(node_id, component_type)
|
||||
}))
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_component_device_lru_node_ids(
|
||||
@@ -1969,8 +2029,9 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(|| self.core().inspect_is_host_evictable_leaf(node_id))
|
||||
}
|
||||
|
||||
fn inspect_is_device_leaf(&self, py: Python<'_>, node_id: NodeId) -> bool {
|
||||
fn inspect_is_device_leaf(&self, py: Python<'_>, node_id: NodeId) -> PyResult<bool> {
|
||||
py.allow_threads(|| self.core().inspect_is_device_leaf(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_get_all_node_ids(&self, py: Python<'_>) -> Vec<NodeId> {
|
||||
@@ -1991,11 +2052,12 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
hash_values: Option<Vec<String>>,
|
||||
) {
|
||||
) -> PyResult<()> {
|
||||
py.allow_threads(move || {
|
||||
self.core()
|
||||
.inspect_set_node_hash_values(node_id, hash_values)
|
||||
});
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_set_component_device_value_raw(
|
||||
@@ -2010,8 +2072,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(move || {
|
||||
self.core()
|
||||
.inspect_set_component_device_value_raw(node_id, component_type, value)
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_set_component_host_value_raw(
|
||||
@@ -2026,8 +2088,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(move || {
|
||||
self.core()
|
||||
.inspect_set_component_host_value_raw(node_id, component_type, value)
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_set_component_device_lock_ref(
|
||||
@@ -2041,8 +2103,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_set_component_device_lock_ref(node_id, component_type, lock_ref)
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_remove_node_from_device_lru(
|
||||
@@ -2055,8 +2117,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_remove_node_from_device_lru(node_id, component_type)
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_insert_node_into_host_lru(
|
||||
@@ -2069,8 +2131,8 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_insert_node_into_host_lru(node_id, component_type)
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_set_component_evictable_size(
|
||||
@@ -2101,8 +2163,9 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inspect_update_duplicate_tracking(&self, py: Python<'_>, node_id: NodeId) {
|
||||
py.allow_threads(|| self.core().inspect_update_duplicate_tracking(node_id));
|
||||
fn inspect_update_duplicate_tracking(&self, py: Python<'_>, node_id: NodeId) -> PyResult<()> {
|
||||
py.allow_threads(|| self.core().inspect_update_duplicate_tracking(node_id))
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
|
||||
fn inspect_advance_insert_walk_once(&self, py: Python<'_>) -> PyResult<()> {
|
||||
@@ -2119,10 +2182,12 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
) -> PyResult<HostEvictionResultBinding> {
|
||||
let component_type = parse_component_type(component_type)?;
|
||||
let target = parse_evict_layer(target)?;
|
||||
let result = py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_evict_component(node_id, component_type, target)
|
||||
});
|
||||
let result = py
|
||||
.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_evict_component(node_id, component_type, target)
|
||||
})
|
||||
.map_err(node_access_error)?;
|
||||
HostEvictionResultBinding::from_eviction_step(py, result)
|
||||
}
|
||||
|
||||
@@ -2139,7 +2204,7 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
self.core()
|
||||
.inspect_validate_cascade_evict(node_id, component_type, target)
|
||||
})
|
||||
.map_err(PyAssertionError::new_err)
|
||||
.map_err(tree_core_assertion_error)
|
||||
}
|
||||
|
||||
fn inspect_cleanup_tombstone_ancestors(
|
||||
@@ -2147,7 +2212,9 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<HostEvictionResultBinding> {
|
||||
let result = py.allow_threads(|| self.core().inspect_cleanup_tombstone_ancestors(node_id));
|
||||
let result = py
|
||||
.allow_threads(|| self.core().inspect_cleanup_tombstone_ancestors(node_id))
|
||||
.map_err(node_access_error)?;
|
||||
HostEvictionResultBinding::from_eviction_step(py, result)
|
||||
}
|
||||
|
||||
@@ -2205,6 +2272,7 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
best_value_len,
|
||||
)
|
||||
});
|
||||
let result = result.map_err(node_access_error)?;
|
||||
MatchResultBinding::from_match_result(py, result)
|
||||
}
|
||||
|
||||
@@ -2213,11 +2281,12 @@ impl<K: ChildKeyType + Send + Sync> TreeCoreBinding<K> {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
write_back: bool,
|
||||
) -> Vec<NodeId> {
|
||||
) -> PyResult<Vec<NodeId>> {
|
||||
py.allow_threads(|| {
|
||||
self.core()
|
||||
.inspect_build_backup_node_ids(node_id, write_back)
|
||||
})
|
||||
.map_err(node_access_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2422,7 +2491,7 @@ macro_rules! tree_core_binding {
|
||||
py: Python<'_>,
|
||||
from_node_id: NodeId,
|
||||
until_node_id: NodeId,
|
||||
) -> PyTensor {
|
||||
) -> PyResult<PyTensor> {
|
||||
self.inner
|
||||
.collect_full_device_indices(py, from_node_id, until_node_id)
|
||||
}
|
||||
@@ -2484,7 +2553,7 @@ macro_rules! tree_core_binding {
|
||||
}
|
||||
|
||||
/// Whether the node's FULL device value has been evicted.
|
||||
fn is_full_device_evicted(&self, py: Python<'_>, node_id: NodeId) -> bool {
|
||||
fn is_full_device_evicted(&self, py: Python<'_>, node_id: NodeId) -> PyResult<bool> {
|
||||
self.inner.is_full_device_evicted(py, node_id)
|
||||
}
|
||||
|
||||
@@ -2739,7 +2808,11 @@ macro_rules! tree_core_binding {
|
||||
}
|
||||
|
||||
/// Bump the reference count on a node's host-side component locks.
|
||||
fn inc_host_lock_ref(&self, py: Python<'_>, node_id: NodeId) -> IncLockRefResultBinding {
|
||||
fn inc_host_lock_ref(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<IncLockRefResultBinding> {
|
||||
self.inner.inc_host_lock_ref(py, node_id)
|
||||
}
|
||||
|
||||
@@ -2810,17 +2883,22 @@ macro_rules! tree_core_binding {
|
||||
py: Python<'_>,
|
||||
node_ids: Vec<NodeId>,
|
||||
ack_id: NodeId,
|
||||
) -> Vec<NodeId> {
|
||||
) -> PyResult<Vec<NodeId>> {
|
||||
self.inner.mark_write_through_pending(py, node_ids, ack_id)
|
||||
}
|
||||
|
||||
/// Clear the write-through-pending mark on the acked nodes.
|
||||
fn finish_write_through(&self, py: Python<'_>, node_ids: Vec<NodeId>, ack_id: NodeId) {
|
||||
fn finish_write_through(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_ids: Vec<NodeId>,
|
||||
ack_id: NodeId,
|
||||
) -> PyResult<()> {
|
||||
self.inner.finish_write_through(py, node_ids, ack_id)
|
||||
}
|
||||
|
||||
/// Clear the in-flight H->D marks on the anchor's root path at ack time.
|
||||
fn finish_load_back(&self, py: Python<'_>, anchor_node_id: NodeId) {
|
||||
fn finish_load_back(&self, py: Python<'_>, anchor_node_id: NodeId) -> PyResult<()> {
|
||||
self.inner.finish_load_back(py, anchor_node_id)
|
||||
}
|
||||
|
||||
@@ -2853,7 +2931,7 @@ macro_rules! tree_core_binding {
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> Option<NodeId> {
|
||||
) -> PyResult<Option<NodeId>> {
|
||||
self.inner.inspect_get_parent_node_id(py, node_id)
|
||||
}
|
||||
|
||||
@@ -2862,22 +2940,34 @@ macro_rules! tree_core_binding {
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> Vec<NodeId> {
|
||||
) -> PyResult<Vec<NodeId>> {
|
||||
self.inner.inspect_get_child_node_ids(py, node_id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "inspection")]
|
||||
fn inspect_get_node_key_length(&self, py: Python<'_>, node_id: NodeId) -> usize {
|
||||
fn inspect_get_node_key_length(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<usize> {
|
||||
self.inner.inspect_get_node_key_length(py, node_id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "inspection")]
|
||||
fn inspect_get_node_token_ids(&self, py: Python<'_>, node_id: NodeId) -> Vec<i64> {
|
||||
fn inspect_get_node_token_ids(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<Vec<i64>> {
|
||||
self.inner.inspect_get_node_token_ids(py, node_id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "inspection")]
|
||||
fn inspect_is_node_key_bigram(&self, py: Python<'_>, node_id: NodeId) -> bool {
|
||||
fn inspect_is_node_key_bigram(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<bool> {
|
||||
self.inner.inspect_is_node_key_bigram(py, node_id)
|
||||
}
|
||||
|
||||
@@ -2904,7 +2994,11 @@ macro_rules! tree_core_binding {
|
||||
}
|
||||
|
||||
#[cfg(feature = "inspection")]
|
||||
fn inspect_get_node_hit_count(&self, py: Python<'_>, node_id: NodeId) -> i64 {
|
||||
fn inspect_get_node_hit_count(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<i64> {
|
||||
self.inner.inspect_get_node_hit_count(py, node_id)
|
||||
}
|
||||
|
||||
@@ -2913,7 +3007,7 @@ macro_rules! tree_core_binding {
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> Option<usize> {
|
||||
) -> PyResult<Option<usize>> {
|
||||
self.inner
|
||||
.inspect_get_write_through_pending_id(py, node_id)
|
||||
}
|
||||
@@ -2969,7 +3063,11 @@ macro_rules! tree_core_binding {
|
||||
}
|
||||
|
||||
#[cfg(feature = "inspection")]
|
||||
fn inspect_is_device_leaf(&self, py: Python<'_>, node_id: NodeId) -> bool {
|
||||
fn inspect_is_device_leaf(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<bool> {
|
||||
self.inner.inspect_is_device_leaf(py, node_id)
|
||||
}
|
||||
|
||||
@@ -2995,7 +3093,7 @@ macro_rules! tree_core_binding {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
hash_values: Option<Vec<String>>,
|
||||
) {
|
||||
) -> PyResult<()> {
|
||||
self.inner
|
||||
.inspect_set_node_hash_values(py, node_id, hash_values)
|
||||
}
|
||||
@@ -3095,7 +3193,11 @@ macro_rules! tree_core_binding {
|
||||
}
|
||||
|
||||
#[cfg(feature = "inspection")]
|
||||
fn inspect_update_duplicate_tracking(&self, py: Python<'_>, node_id: NodeId) {
|
||||
fn inspect_update_duplicate_tracking(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
) -> PyResult<()> {
|
||||
self.inner.inspect_update_duplicate_tracking(py, node_id)
|
||||
}
|
||||
|
||||
@@ -3171,7 +3273,7 @@ macro_rules! tree_core_binding {
|
||||
py: Python<'_>,
|
||||
node_id: NodeId,
|
||||
write_back: bool,
|
||||
) -> Vec<NodeId> {
|
||||
) -> PyResult<Vec<NodeId>> {
|
||||
self.inner
|
||||
.inspect_build_backup_node_ids(py, node_id, write_back)
|
||||
}
|
||||
|
||||
@@ -119,6 +119,8 @@ fn finalize_match_result_default_returns_result_unchanged() {
|
||||
let out = DefaultComponentForTest.finalize_match_result_in_tree_core(
|
||||
&tc,
|
||||
result,
|
||||
tc.arena.root(),
|
||||
tc.arena.root(),
|
||||
&MatchPrefixParams {
|
||||
key: &Vec::new(),
|
||||
namespace: Default::default(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::components::FULL;
|
||||
use crate::node::NodeAccessError;
|
||||
use crate::test_utils::accumulate_step;
|
||||
use crate::unified_tree_core::CacheInitParams;
|
||||
|
||||
@@ -392,14 +393,16 @@ fn host_drive_reclaims_coexisting_host_values_while_sparing_the_device_leaf() {
|
||||
let leaf_handle = tc
|
||||
.match_prefix(&match_params(&vec![1, 2, 3]))
|
||||
.best_match_node_id;
|
||||
let leaf = tc.arena.resolve(leaf_handle);
|
||||
let leaf = tc.arena.resolve(leaf_handle).expect("live test node");
|
||||
let parent = tc.arena.node(leaf).parent();
|
||||
tc.commit_backup(
|
||||
tc.arena.node(parent).id,
|
||||
Tensor::from_slice(&[20i64, 21]),
|
||||
HashMap::new(),
|
||||
);
|
||||
tc.commit_backup(leaf_handle, Tensor::from_slice(&[22i64]), HashMap::new());
|
||||
)
|
||||
.expect("live test node");
|
||||
tc.commit_backup(leaf_handle, Tensor::from_slice(&[22i64]), HashMap::new())
|
||||
.expect("live test node");
|
||||
assert!(tc.evictable_host_leaves.is_empty());
|
||||
|
||||
let (mut tr, mut df, mut hf) = (tracker(), frees(), frees());
|
||||
@@ -424,8 +427,10 @@ fn host_drive_spares_coexisting_host_values_under_an_in_flight_transfer() {
|
||||
let handle = tc
|
||||
.match_prefix(&match_params(&vec![1, 2]))
|
||||
.best_match_node_id;
|
||||
tc.commit_backup(handle, Tensor::from_slice(&[20i64, 21]), HashMap::new());
|
||||
tc.mark_write_through_pending(vec![handle], /* ack_id = */ handle);
|
||||
tc.commit_backup(handle, Tensor::from_slice(&[20i64, 21]), HashMap::new())
|
||||
.expect("live test node");
|
||||
tc.mark_write_through_pending(vec![handle], /* ack_id = */ handle)
|
||||
.expect("live test node");
|
||||
|
||||
let (mut tr, mut df, mut hf) = (tracker(), frees(), frees());
|
||||
accumulate_step(
|
||||
@@ -435,9 +440,14 @@ fn host_drive_spares_coexisting_host_values_under_an_in_flight_transfer() {
|
||||
&mut hf,
|
||||
);
|
||||
assert_eq!(tr[&FULL], 0);
|
||||
assert!(tc.arena.node(tc.arena.resolve(handle)).has_host_value(FULL));
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(tc.arena.resolve(handle).expect("live test node"))
|
||||
.has_host_value(FULL)
|
||||
);
|
||||
|
||||
tc.finish_write_through(vec![handle], handle);
|
||||
tc.finish_write_through(vec![handle], handle)
|
||||
.expect("live test node");
|
||||
accumulate_step(
|
||||
tc.drive_host_eviction(FULL, /* num_tokens = */ 2),
|
||||
&mut tr,
|
||||
@@ -445,7 +455,11 @@ fn host_drive_spares_coexisting_host_values_under_an_in_flight_transfer() {
|
||||
&mut hf,
|
||||
);
|
||||
assert_eq!(tr[&FULL], 2);
|
||||
assert!(!tc.arena.node(tc.arena.resolve(handle)).has_host_value(FULL));
|
||||
assert!(
|
||||
!tc.arena
|
||||
.node(tc.arena.resolve(handle).expect("live test node"))
|
||||
.has_host_value(FULL)
|
||||
);
|
||||
tc.sanity_check(&[], &[]);
|
||||
}
|
||||
|
||||
@@ -610,7 +624,9 @@ fn lock_chain(tc: &mut UnifiedTreeCore<Vec<i64>>) -> (NodeIdx_, NodeIdx_) {
|
||||
fn inc_lock_ref_locks_the_device_path() {
|
||||
let mut tc = core();
|
||||
let (n1, n2) = lock_chain(&mut tc);
|
||||
let result = tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, Some(5));
|
||||
assert!(result.skip_lock_node_ids.is_empty());
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 1);
|
||||
@@ -625,8 +641,11 @@ fn inc_lock_ref_locks_the_device_path() {
|
||||
fn inc_lock_ref_again_only_bumps_the_refs() {
|
||||
let mut tc = core();
|
||||
let (n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
let result = tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, Some(0));
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 2);
|
||||
assert_eq!(tc.arena.device_lock_ref(n2, FULL), 2);
|
||||
@@ -640,8 +659,11 @@ fn inc_lock_ref_counts_only_newly_locked_nodes() {
|
||||
// n1 is already locked via its own path; locking n2 moves only n2's tokens.
|
||||
let mut tc = core();
|
||||
let (n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_lock_ref(tc.arena.node(n1).id);
|
||||
let result = tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n1).id)
|
||||
.expect("live test node");
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, Some(3));
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 2);
|
||||
assert_eq!(tc.arena.device_lock_ref(n2, FULL), 1);
|
||||
@@ -686,7 +708,9 @@ fn inc_lock_ref_collects_the_evicted_bottom_segment() {
|
||||
.set_device_value(n1, FULL, Tensor::from_slice(&[0i64, 1]));
|
||||
tc.component_state_mut(FULL).evictable_size = 2;
|
||||
tc.evictable_device_leaves.add(n1);
|
||||
let result = tc.inc_lock_ref(tc.arena.node(n3).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(n3).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, Some(2));
|
||||
assert_eq!(
|
||||
result.skip_lock_node_ids[&FULL],
|
||||
@@ -703,7 +727,9 @@ fn inc_lock_ref_collects_the_evicted_bottom_segment() {
|
||||
fn lock_round_trips_on_a_root_anchor_are_noops() {
|
||||
let mut tc = core();
|
||||
let root = tc.arena.root();
|
||||
let result = tc.inc_lock_ref(tc.arena.node(root).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(root).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, Some(0));
|
||||
assert!(result.skip_lock_node_ids.is_empty());
|
||||
// The protected root keeps its construction-time lock through the pair.
|
||||
@@ -712,7 +738,8 @@ fn lock_round_trips_on_a_root_anchor_are_noops() {
|
||||
tc.arena.node(root).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(root, FULL), 1);
|
||||
}
|
||||
|
||||
@@ -732,7 +759,9 @@ fn lock_walks_stop_at_the_root_of_a_salted_chain() {
|
||||
tc.arena
|
||||
.set_device_value(n1, FULL, Tensor::from_slice(&[0i64, 1]));
|
||||
tc.component_state_mut(FULL).evictable_size = 2;
|
||||
let result = tc.inc_lock_ref(tc.arena.node(n1).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(n1).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, Some(2));
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 1);
|
||||
// The root keeps its construction-time lock untouched.
|
||||
@@ -742,7 +771,8 @@ fn lock_walks_stop_at_the_root_of_a_salted_chain() {
|
||||
tc.arena.node(n1).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(lora, FULL), 1);
|
||||
}
|
||||
@@ -767,7 +797,9 @@ fn lock_walks_treat_a_present_but_empty_value_as_device_on() {
|
||||
ValueSlotIdx::device(FULL),
|
||||
Tensor::from_slice(&empty),
|
||||
);
|
||||
let result = tc.inc_lock_ref(tc.arena.node(n1).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(n1).id)
|
||||
.expect("live test node");
|
||||
// A present-but-empty value is device-on (Python `value is not None`):
|
||||
// locked, zero tokens moved.
|
||||
assert_eq!(result.delta, Some(0));
|
||||
@@ -778,7 +810,8 @@ fn lock_walks_treat_a_present_but_empty_value_as_device_on() {
|
||||
tc.arena.node(n1).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 0);
|
||||
let state = tc.component_state(FULL);
|
||||
assert_eq!(state.evictable_size, 0);
|
||||
@@ -789,12 +822,14 @@ fn lock_walks_treat_a_present_but_empty_value_as_device_on() {
|
||||
fn dec_lock_ref_unlocks_and_restores_sizes() {
|
||||
let mut tc = core();
|
||||
let (n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
tc.dec_lock_ref(
|
||||
tc.arena.node(n2).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(n2, FULL), 0);
|
||||
let state = tc.component_state(FULL);
|
||||
@@ -839,7 +874,9 @@ fn dec_lock_ref_replays_the_skip_set() {
|
||||
tc.arena
|
||||
.set_device_value(n1, FULL, Tensor::from_slice(&[0i64, 1]));
|
||||
tc.component_state_mut(FULL).evictable_size = 2;
|
||||
let result = tc.inc_lock_ref(tc.arena.node(n3).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(n3).id)
|
||||
.expect("live test node");
|
||||
let params = DecLockRefParams {
|
||||
skip_lock_node_ids: result.skip_lock_node_ids,
|
||||
..Default::default()
|
||||
@@ -849,7 +886,8 @@ fn dec_lock_ref_replays_the_skip_set() {
|
||||
tc.arena.node(n3).id,
|
||||
Some(¶ms),
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(n1, FULL), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(n2, FULL), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(n3, FULL), 0);
|
||||
@@ -896,7 +934,9 @@ fn temp_lock_skips_the_evicted_anchor_and_mirrors_on_release() {
|
||||
.set_device_value(y, FULL, Tensor::from_slice(&[0i64]));
|
||||
tc.component_state_mut(FULL).evictable_size = 3;
|
||||
// The temp lock records the evicted anchor and locks only its ancestors.
|
||||
let temp_lock = tc.inc_lock_ref(tc.arena.node(anchor).id);
|
||||
let temp_lock = tc
|
||||
.inc_lock_ref(tc.arena.node(anchor).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(anchor, FULL), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(y, FULL), 1);
|
||||
assert_eq!(tc.arena.device_lock_ref(a, FULL), 1);
|
||||
@@ -907,7 +947,9 @@ fn temp_lock_skips_the_evicted_anchor_and_mirrors_on_release() {
|
||||
// A load-back restores the anchor; the second acquire covers it.
|
||||
tc.arena
|
||||
.set_device_value(anchor, FULL, Tensor::from_slice(&[0i64]));
|
||||
let second_lock = tc.inc_lock_ref(tc.arena.node(anchor).id);
|
||||
let second_lock = tc
|
||||
.inc_lock_ref(tc.arena.node(anchor).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(anchor, FULL), 1);
|
||||
assert_eq!(tc.arena.device_lock_ref(y, FULL), 2);
|
||||
assert_eq!(tc.arena.device_lock_ref(a, FULL), 2);
|
||||
@@ -920,7 +962,8 @@ fn temp_lock_skips_the_evicted_anchor_and_mirrors_on_release() {
|
||||
tc.arena.node(anchor).id,
|
||||
Some(&temp_params),
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(anchor, FULL), 1);
|
||||
assert_eq!(tc.arena.device_lock_ref(y, FULL), 1);
|
||||
assert_eq!(tc.arena.device_lock_ref(a, FULL), 1);
|
||||
@@ -932,7 +975,8 @@ fn temp_lock_skips_the_evicted_anchor_and_mirrors_on_release() {
|
||||
tc.arena.node(anchor).id,
|
||||
Some(&second_params),
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(anchor, FULL), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(y, FULL), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(a, FULL), 0);
|
||||
@@ -965,24 +1009,28 @@ fn dec_lock_ref_panics_without_replaying_the_skip_set() {
|
||||
tc.arena
|
||||
.set_device_value(n1, FULL, Tensor::from_slice(&[0i64, 1]));
|
||||
tc.component_state_mut(FULL).evictable_size = 2;
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
tc.dec_lock_ref(
|
||||
tc.arena.node(n2).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dec_lock_ref_with_skip_swa_still_releases_full() {
|
||||
let mut tc = core();
|
||||
let (_n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
tc.dec_lock_ref(
|
||||
tc.arena.node(n2).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ true,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(n2, FULL), 0);
|
||||
}
|
||||
|
||||
@@ -991,13 +1039,16 @@ fn nested_locks_release_pairwise() {
|
||||
// Two acquires then two releases: sizes move only on the outermost pair.
|
||||
let mut tc = core();
|
||||
let (_n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
tc.dec_lock_ref(
|
||||
tc.arena.node(n2).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
let state = tc.component_state(FULL);
|
||||
assert_eq!(state.evictable_size, 0);
|
||||
assert_eq!(state.protected_size, 5);
|
||||
@@ -1006,7 +1057,8 @@ fn nested_locks_release_pairwise() {
|
||||
tc.arena.node(n2).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
let state = tc.component_state(FULL);
|
||||
assert_eq!(state.evictable_size, 5);
|
||||
assert_eq!(state.protected_size, 0);
|
||||
@@ -1022,7 +1074,8 @@ fn dec_lock_ref_panics_on_an_unlocked_node() {
|
||||
tc.arena.node(n2).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1052,7 +1105,8 @@ fn inc_lock_ref_panics_on_an_evicted_ancestor() {
|
||||
tc.arena
|
||||
.set_device_value(n2, FULL, Tensor::from_slice(&[0i64, 1, 2]));
|
||||
tc.component_state_mut(FULL).evictable_size = 3;
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1071,7 +1125,8 @@ fn inc_lock_ref_panics_when_evictable_size_is_unaccounted() {
|
||||
.unwrap();
|
||||
tc.arena
|
||||
.set_device_value(n1, FULL, Tensor::from_slice(&[0i64]));
|
||||
tc.inc_lock_ref(tc.arena.node(n1).id);
|
||||
tc.inc_lock_ref(tc.arena.node(n1).id)
|
||||
.expect("live test node");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1087,7 +1142,8 @@ fn dec_lock_ref_panics_on_protected_underflow() {
|
||||
tc.arena.node(n2).id,
|
||||
/* params = */ None,
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
}
|
||||
|
||||
fn write_back_core() -> UnifiedTreeCore<Vec<i64>> {
|
||||
@@ -1123,7 +1179,9 @@ fn inc_host_lock_ref_pins_the_backuped_anchor() {
|
||||
let mut tc = core();
|
||||
let node = host_lock_anchor(&mut tc);
|
||||
tc.component_state_mut(FULL).evictable_size = 7;
|
||||
let result = tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
let result = tc
|
||||
.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, None);
|
||||
assert!(result.skip_lock_node_ids.is_empty());
|
||||
assert_eq!(tc.arena.host_lock_ref(node, FULL), 1);
|
||||
@@ -1139,8 +1197,10 @@ fn inc_host_lock_ref_pins_the_backuped_anchor() {
|
||||
fn inc_host_lock_ref_again_only_bumps_the_counter() {
|
||||
let mut tc = core();
|
||||
let node = host_lock_anchor(&mut tc);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(node, FULL), 2);
|
||||
assert!(!tc.evictable_host_leaves.contains(node));
|
||||
}
|
||||
@@ -1172,7 +1232,8 @@ fn inc_host_lock_ref_pins_only_the_anchor_not_its_ancestors() {
|
||||
.set_host_value(n1, FULL, Tensor::from_slice(&[0i64]));
|
||||
tc.arena
|
||||
.set_host_value(n2, FULL, Tensor::from_slice(&[0i64]));
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(n2, FULL), 1);
|
||||
assert_eq!(tc.arena.host_lock_ref(n1, FULL), 0);
|
||||
}
|
||||
@@ -1181,7 +1242,8 @@ fn inc_host_lock_ref_pins_only_the_anchor_not_its_ancestors() {
|
||||
fn inc_host_lock_ref_skips_an_anchor_without_a_host_value() {
|
||||
let mut tc = core();
|
||||
let (_n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(n2, FULL), 0);
|
||||
}
|
||||
|
||||
@@ -1189,10 +1251,13 @@ fn inc_host_lock_ref_skips_an_anchor_without_a_host_value() {
|
||||
fn host_lock_round_trips_on_a_root_anchor_are_noops() {
|
||||
let mut tc = core();
|
||||
let root = tc.arena.root();
|
||||
let result = tc.inc_host_lock_ref(tc.arena.node(root).id);
|
||||
let result = tc
|
||||
.inc_host_lock_ref(tc.arena.node(root).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(result.delta, None);
|
||||
assert_eq!(tc.arena.host_lock_ref(root, FULL), 0);
|
||||
tc.dec_host_lock_ref(tc.arena.node(root).id, /* params = */ None);
|
||||
tc.dec_host_lock_ref(tc.arena.node(root).id, /* params = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(root, FULL), 0);
|
||||
}
|
||||
|
||||
@@ -1200,7 +1265,8 @@ fn host_lock_round_trips_on_a_root_anchor_are_noops() {
|
||||
fn inc_host_lock_ref_under_write_back_pins_a_device_only_anchor() {
|
||||
let mut tc = write_back_core();
|
||||
let (_n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(n2, FULL), 1);
|
||||
// The write-back host lock is a pure counter: no size shifts.
|
||||
let state = tc.component_state(FULL);
|
||||
@@ -1213,8 +1279,10 @@ fn dec_host_lock_ref_unpins_and_restores_the_h_leaf_set() {
|
||||
let mut tc = core();
|
||||
let node = host_lock_anchor(&mut tc);
|
||||
tc.component_state_mut(FULL).evictable_size = 7;
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(node, FULL), 0);
|
||||
assert!(tc.evictable_host_leaves.contains(node));
|
||||
let state = tc.component_state(FULL);
|
||||
@@ -1226,7 +1294,8 @@ fn dec_host_lock_ref_unpins_and_restores_the_h_leaf_set() {
|
||||
fn dec_host_lock_ref_on_an_unlocked_anchor_is_a_noop() {
|
||||
let mut tc = core();
|
||||
let node = host_lock_anchor(&mut tc);
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None);
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(node, FULL), 0);
|
||||
}
|
||||
|
||||
@@ -1235,9 +1304,11 @@ fn dec_host_lock_ref_keeps_the_counter_when_the_host_value_is_gone() {
|
||||
// A host-evicted anchor keeps its pin count under write-through.
|
||||
let mut tc = core();
|
||||
let node = host_lock_anchor(&mut tc);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
let _ = tc.arena.take_host_value(node, FULL);
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None);
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(node, FULL), 1);
|
||||
}
|
||||
|
||||
@@ -1245,8 +1316,10 @@ fn dec_host_lock_ref_keeps_the_counter_when_the_host_value_is_gone() {
|
||||
fn host_lock_round_trip_under_write_back_is_a_pure_counter() {
|
||||
let mut tc = write_back_core();
|
||||
let (_n1, n2) = lock_chain(&mut tc);
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id);
|
||||
tc.dec_host_lock_ref(tc.arena.node(n2).id, /* params = */ None);
|
||||
tc.inc_host_lock_ref(tc.arena.node(n2).id)
|
||||
.expect("live test node");
|
||||
tc.dec_host_lock_ref(tc.arena.node(n2).id, /* params = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(n2, FULL), 0);
|
||||
let state = tc.component_state(FULL);
|
||||
assert_eq!(state.evictable_size, 5);
|
||||
@@ -1270,7 +1343,8 @@ fn acquire_host_arm_updates_the_h_leaf_set_without_the_dispatcher() {
|
||||
fn release_host_arm_updates_the_h_leaf_set_without_the_dispatcher() {
|
||||
let mut tc = core();
|
||||
let node = host_lock_anchor(&mut tc);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
FullComponent.release_component_lock(
|
||||
&mut tc, node, /* params = */ None, /* lock_host = */ true,
|
||||
);
|
||||
@@ -1281,12 +1355,16 @@ fn release_host_arm_updates_the_h_leaf_set_without_the_dispatcher() {
|
||||
fn nested_host_locks_release_pairwise() {
|
||||
let mut tc = core();
|
||||
let node = host_lock_anchor(&mut tc);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id);
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None);
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
tc.inc_host_lock_ref(tc.arena.node(node).id)
|
||||
.expect("live test node");
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(node, FULL), 1);
|
||||
assert!(!tc.evictable_host_leaves.contains(node));
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None);
|
||||
tc.dec_host_lock_ref(tc.arena.node(node).id, /* params = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(node, FULL), 0);
|
||||
assert!(tc.evictable_host_leaves.contains(node));
|
||||
}
|
||||
@@ -1578,9 +1656,19 @@ fn host_hit_chain() -> (UnifiedTreeCore<Vec<i64>>, NodeIdx_, NodeIdx_) {
|
||||
}
|
||||
|
||||
fn finalize(tc: &UnifiedTreeCore<Vec<i64>>, result: MatchResult) -> MatchResult {
|
||||
let last_device_node_idx = tc
|
||||
.arena
|
||||
.resolve(result.last_device_node_id)
|
||||
.expect("live test device node");
|
||||
let best_match_node_idx = tc
|
||||
.arena
|
||||
.resolve(result.best_match_node_id)
|
||||
.expect("live test best-match node");
|
||||
FullComponent.finalize_match_result_in_tree_core(
|
||||
tc,
|
||||
result,
|
||||
last_device_node_idx,
|
||||
best_match_node_idx,
|
||||
&MatchPrefixParams {
|
||||
key: &Vec::new(),
|
||||
namespace: Default::default(),
|
||||
@@ -1791,18 +1879,62 @@ fn match_validator_panics_on_missing_node() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "is not allocated")]
|
||||
fn finalize_panics_on_missing_best_match_node() {
|
||||
fn inspect_finalize_rejects_missing_match_nodes() {
|
||||
let tc = core();
|
||||
let root = tc.arena.root();
|
||||
finalize(
|
||||
&tc,
|
||||
let root_id = tc.arena.node(root).id;
|
||||
let params = MatchPrefixParams {
|
||||
key: &Vec::new(),
|
||||
namespace: Default::default(),
|
||||
};
|
||||
let result = tc.inspect_finalize_component_match_result(
|
||||
FULL,
|
||||
MatchResult {
|
||||
last_device_node_id: tc.arena.node(root).id,
|
||||
last_device_node_id: root_id,
|
||||
last_host_node_id: root_id,
|
||||
best_match_node_id: 999,
|
||||
host_hit_length: 0,
|
||||
..tc.empty_match_result()
|
||||
},
|
||||
¶ms,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
assert!(matches!(result, Err(NodeAccessError { node_id: 999 })));
|
||||
|
||||
let result = tc.inspect_finalize_component_match_result(
|
||||
FULL,
|
||||
MatchResult {
|
||||
last_device_node_id: 998,
|
||||
last_host_node_id: root_id,
|
||||
best_match_node_id: root_id,
|
||||
host_hit_length: 0,
|
||||
..tc.empty_match_result()
|
||||
},
|
||||
¶ms,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
assert!(matches!(result, Err(NodeAccessError { node_id: 998 })));
|
||||
|
||||
let result = tc.inspect_finalize_component_match_result(
|
||||
FULL,
|
||||
MatchResult {
|
||||
last_device_node_id: root_id,
|
||||
last_host_node_id: 997,
|
||||
best_match_node_id: root_id,
|
||||
host_hit_length: 0,
|
||||
..tc.empty_match_result()
|
||||
},
|
||||
¶ms,
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
assert!(matches!(result, Err(NodeAccessError { node_id: 997 })));
|
||||
|
||||
assert!(
|
||||
tc.inspect_finalize_component_match_result(FULL, tc.empty_match_result(), ¶ms, &[], 0,)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ fn hybrid_lock_core() -> (UnifiedTreeCore<Vec<i64>>, NodeIdx_, NodeIdx_) {
|
||||
for (node, full_slot, swa_slot, mamba_slot) in [(parent, 10, 20, 30), (leaf, 11, 21, 31)] {
|
||||
tc.arena
|
||||
.set_device_value(node, FULL, Tensor::from_slice(&[full_slot]));
|
||||
tc.set_component_device_value(tc.arena.node(node).id, SWA, Tensor::from_slice(&[swa_slot]));
|
||||
tc.set_component_device_value(tc.arena.node(node).id, SWA, Tensor::from_slice(&[swa_slot]))
|
||||
.expect("live test node");
|
||||
set_mamba_device(&mut tc, node, mamba_slot);
|
||||
tc.update_evictable_leaf_sets_(node);
|
||||
}
|
||||
@@ -229,7 +230,8 @@ fn device_value_round_trips_through_the_component() {
|
||||
let [a] = chain::<1>(&mut tc);
|
||||
let mamba = mamba_component();
|
||||
assert!(tc.arena.try_device_value(a, MAMBA).is_none());
|
||||
tc.set_component_device_value(tc.arena.node(a).id, MAMBA, Tensor::from_slice(&[42i64]));
|
||||
tc.set_component_device_value(tc.arena.node(a).id, MAMBA, Tensor::from_slice(&[42i64]))
|
||||
.expect("live test node");
|
||||
assert!(
|
||||
tc.arena
|
||||
.try_device_value(a, MAMBA)
|
||||
@@ -314,7 +316,9 @@ fn skip_aware_lock_records_only_the_mamba_target() {
|
||||
let (mut tc, parent, leaf) = hybrid_lock_core();
|
||||
let leaf_handle = tc.arena.node(leaf).id;
|
||||
|
||||
let result = tc.inc_lock_ref_with_skip(leaf_handle, &[MAMBA]);
|
||||
let result = tc
|
||||
.inc_lock_ref_with_skip(leaf_handle, &[MAMBA])
|
||||
.expect("live test node");
|
||||
|
||||
assert_eq!(result.skip_lock_node_ids[&MAMBA].len(), 1);
|
||||
assert!(result.skip_lock_node_ids[&MAMBA].contains(&leaf_handle));
|
||||
@@ -333,7 +337,8 @@ fn skip_aware_lock_records_only_the_mamba_target() {
|
||||
..Default::default()
|
||||
}),
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.node(parent).device_lock_ref(FULL), 0);
|
||||
assert_eq!(tc.arena.node(leaf).device_lock_ref(FULL), 0);
|
||||
}
|
||||
@@ -342,8 +347,10 @@ fn skip_aware_lock_records_only_the_mamba_target() {
|
||||
fn swa_only_release_honors_a_skipped_mamba_target() {
|
||||
let (mut tc, _parent, leaf) = hybrid_lock_core();
|
||||
let leaf_handle = tc.arena.node(leaf).id;
|
||||
let owner = tc.inc_lock_ref(leaf_handle);
|
||||
let skipped = tc.inc_lock_ref_with_skip(leaf_handle, &[MAMBA]);
|
||||
let owner = tc.inc_lock_ref(leaf_handle).expect("live test node");
|
||||
let skipped = tc
|
||||
.inc_lock_ref_with_skip(leaf_handle, &[MAMBA])
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.node(leaf).device_lock_ref(MAMBA), 1);
|
||||
|
||||
let mut device_frees = HashMap::new();
|
||||
@@ -354,7 +361,8 @@ fn swa_only_release_honors_a_skipped_mamba_target() {
|
||||
Some(&skipped.skip_lock_node_ids),
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
|
||||
assert!(device_frees.is_empty());
|
||||
assert!(host_frees.is_empty());
|
||||
@@ -370,7 +378,8 @@ fn swa_only_release_honors_a_skipped_mamba_target() {
|
||||
leaf_handle,
|
||||
Some(&skipped_params),
|
||||
/* skip_swa = */ true,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
let owner_params = DecLockRefParams {
|
||||
swa_uuid_for_lock: owner.swa_uuid_for_lock,
|
||||
skip_lock_node_ids: owner.skip_lock_node_ids,
|
||||
@@ -380,7 +389,8 @@ fn swa_only_release_honors_a_skipped_mamba_target() {
|
||||
leaf_handle,
|
||||
Some(&owner_params),
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.protected_size_(MAMBA), 0);
|
||||
}
|
||||
|
||||
@@ -483,14 +493,14 @@ fn insert_attaches_the_donated_slot_to_the_new_leaf() {
|
||||
.best_match_node_id;
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(tc.arena.resolve(leaf))
|
||||
.node(tc.arena.resolve(leaf).expect("live test node"))
|
||||
.try_device_value(MAMBA)
|
||||
.unwrap()
|
||||
.equal(&Tensor::from_slice(&[7i64]))
|
||||
);
|
||||
assert!(
|
||||
tc.device_lru_list(MAMBA)
|
||||
.in_list(Some(tc.arena.resolve(leaf)))
|
||||
.in_list(Some(tc.arena.resolve(leaf).expect("live test node")))
|
||||
);
|
||||
assert_eq!(tc.evictable_size_(MAMBA), 1);
|
||||
}
|
||||
@@ -508,7 +518,7 @@ fn reinsert_keeps_the_existing_slot_and_flags_the_caller() {
|
||||
// The original slot stays; the caller frees the unused donated one.
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(tc.arena.resolve(leaf))
|
||||
.node(tc.arena.resolve(leaf).expect("live test node"))
|
||||
.try_device_value(MAMBA)
|
||||
.unwrap()
|
||||
.equal(&Tensor::from_slice(&[7i64]))
|
||||
@@ -522,7 +532,8 @@ fn reinsert_full_backed_target_schedules_mamba_only_backup() {
|
||||
let key = vec![1, 2];
|
||||
tc.insert(&insert_params_mamba(&key, &[10, 11], Some(7)));
|
||||
let leaf = tc.match_prefix(&match_params(&key)).best_match_node_id;
|
||||
tc.commit_backup(leaf, Tensor::from_slice(&[100i64, 101]), HashMap::new());
|
||||
tc.commit_backup(leaf, Tensor::from_slice(&[100i64, 101]), HashMap::new())
|
||||
.expect("live test node");
|
||||
|
||||
let result = tc.insert(&insert_params_mamba(&key, &[20, 21], Some(8)));
|
||||
let backups = result
|
||||
@@ -536,7 +547,7 @@ fn reinsert_full_backed_target_schedules_mamba_only_backup() {
|
||||
assert_eq!(backups.len(), 1);
|
||||
assert_eq!(backups[0].node_ids, vec![leaf]);
|
||||
|
||||
let (full_device_indices, comp_xfers) = tc.build_backup_spec(leaf);
|
||||
let (full_device_indices, comp_xfers) = tc.build_backup_spec(leaf).expect("live test node");
|
||||
assert_eq!(full_device_indices.numel(), 0);
|
||||
let mamba_xfers = &comp_xfers[&MAMBA];
|
||||
assert_eq!(mamba_xfers.len(), 1);
|
||||
@@ -548,7 +559,8 @@ fn reinsert_full_backed_target_schedules_mamba_only_backup() {
|
||||
.equal(&Tensor::from_slice(&[7i64]))
|
||||
);
|
||||
|
||||
tc.mark_write_through_pending(vec![leaf], /* ack_id = */ leaf);
|
||||
tc.mark_write_through_pending(vec![leaf], /* ack_id = */ leaf)
|
||||
.expect("live test node");
|
||||
let pending = tc.insert(&insert_params_mamba(&key, &[30, 31], Some(9)));
|
||||
assert!(
|
||||
!pending
|
||||
@@ -779,11 +791,25 @@ fn device_walk_advances_one_allocator_mutation_per_call() {
|
||||
// The internal node is a complete step so its free can be reused before
|
||||
// the walk hands out another victim.
|
||||
assert_eq!(first, None);
|
||||
assert!(!tc.arena.node(tc.arena.resolve(a)).has_device_value(MAMBA));
|
||||
assert!(tc.arena.node(tc.arena.resolve(b)).has_device_value(MAMBA));
|
||||
assert!(tc.arena.has_device_value(tc.arena.resolve(a), FULL));
|
||||
assert!(
|
||||
!tc.arena
|
||||
.node(tc.arena.resolve(a).expect("live test node"))
|
||||
.has_device_value(MAMBA)
|
||||
);
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(tc.arena.resolve(b).expect("live test node"))
|
||||
.has_device_value(MAMBA)
|
||||
);
|
||||
assert!(
|
||||
tc.arena
|
||||
.has_device_value(tc.arena.resolve(a).expect("live test node"), FULL)
|
||||
);
|
||||
assert_eq!(tracker[&MAMBA], 1);
|
||||
assert!(!tc.device_lru_list(MAMBA).in_list(Some(tc.arena.resolve(a))));
|
||||
assert!(
|
||||
!tc.device_lru_list(MAMBA)
|
||||
.in_list(Some(tc.arena.resolve(a).expect("live test node")))
|
||||
);
|
||||
|
||||
let (second, step) = tc.evict_device_next_node(MAMBA, &tracker);
|
||||
accumulate_step(step, &mut tracker, &mut device_frees, &mut host_frees);
|
||||
@@ -801,7 +827,7 @@ fn device_walk_skips_locked_nodes() {
|
||||
let b = tc
|
||||
.match_prefix(&match_params(&vec![1, 2]))
|
||||
.best_match_node_id;
|
||||
let a_idx = tc.arena.resolve(a);
|
||||
let a_idx = tc.arena.resolve(a).expect("live test node");
|
||||
mamba_component().acquire_component_lock(
|
||||
&mut tc,
|
||||
a_idx,
|
||||
@@ -816,7 +842,11 @@ fn device_walk_skips_locked_nodes() {
|
||||
accumulate_step(step, &mut tracker, &mut device_frees, &mut host_frees);
|
||||
// The locked internal node stays; the cursor starts on the leaf.
|
||||
assert_eq!(next, Some(b));
|
||||
assert!(tc.arena.node(tc.arena.resolve(a)).has_device_value(MAMBA));
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(tc.arena.resolve(a).expect("live test node"))
|
||||
.has_device_value(MAMBA)
|
||||
);
|
||||
assert_eq!(tracker[&MAMBA], 0);
|
||||
tc.evict_device_end(MAMBA);
|
||||
}
|
||||
@@ -877,12 +907,16 @@ fn host_eviction_takes_a_host_leaf_atomically() {
|
||||
Tensor::from_slice(&[100i64]),
|
||||
vec!["h0".to_string()],
|
||||
)
|
||||
.expect("live test node")
|
||||
.inserted_host_node
|
||||
.unwrap();
|
||||
let leaf_idx = tc.arena.resolve(leaf);
|
||||
let leaf_idx = tc.arena.resolve(leaf).expect("live test node");
|
||||
set_mamba_host(&mut tc, leaf_idx, 8);
|
||||
tc.host_lru_list_mut(MAMBA).insert_mru(leaf_idx);
|
||||
assert!(tc.evictable_host_leaves.contains(tc.arena.resolve(leaf)));
|
||||
assert!(
|
||||
tc.evictable_host_leaves
|
||||
.contains(tc.arena.resolve(leaf).expect("live test node"))
|
||||
);
|
||||
let mut tracker = HashMap::from([(MAMBA, 0)]);
|
||||
let mut device_frees = HashMap::new();
|
||||
let mut host_frees = HashMap::new();
|
||||
@@ -897,7 +931,7 @@ fn host_eviction_takes_a_host_leaf_atomically() {
|
||||
assert_eq!(tracker[&MAMBA], 1);
|
||||
assert!(host_frees[&MAMBA][0].equal(&Tensor::from_slice(&[8i64])));
|
||||
assert!(host_frees[&FULL][0].equal(&Tensor::from_slice(&[100i64])));
|
||||
assert!(tc.arena.try_resolve(leaf).is_none());
|
||||
assert!(tc.arena.resolve(leaf).is_err());
|
||||
assert!(!tc.host_lru_list(MAMBA).in_list(Some(leaf_idx)));
|
||||
}
|
||||
|
||||
@@ -996,6 +1030,7 @@ fn backup_host_build_carries_the_device_slot() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.unwrap();
|
||||
assert_eq!(transfers.len(), 1);
|
||||
assert_eq!(transfers[0].name, PoolName::Mamba);
|
||||
@@ -1026,6 +1061,7 @@ fn backup_host_build_carries_the_device_slot() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
@@ -1045,6 +1081,7 @@ fn load_back_build_restores_the_host_only_node() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.unwrap();
|
||||
assert_eq!(transfers.len(), 1);
|
||||
assert!(
|
||||
@@ -1073,6 +1110,7 @@ fn load_back_build_skips_device_backed_and_bare_nodes() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
@@ -1127,7 +1165,8 @@ fn backup_host_commit_stores_the_host_slot_once() {
|
||||
&mut cache_actions,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(a)
|
||||
@@ -1150,7 +1189,8 @@ fn backup_host_commit_stores_the_host_slot_once() {
|
||||
&mut cache_actions,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(a)
|
||||
@@ -1183,7 +1223,8 @@ fn load_back_commit_moves_the_node_onto_the_device_tier() {
|
||||
&mut cache_actions,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(a)
|
||||
@@ -1204,15 +1245,17 @@ fn mamba_device_eviction_skips_a_load_back_pinned_node() {
|
||||
let [n] = chain::<1>(&mut tc);
|
||||
set_full_host(&mut tc, n, 10);
|
||||
set_mamba_host(&mut tc, n, 20);
|
||||
let (kv_xfer, mut comp_xfers) =
|
||||
tc.build_load_back_spec(tc.arena.node(n).id, /* req = */ None);
|
||||
let (kv_xfer, mut comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(n).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
comp_xfers.get_mut(&MAMBA).unwrap()[0].device_indices = Some(Tensor::from_slice(&[40i64]));
|
||||
tc.commit_load_back(
|
||||
tc.arena.node(n).id,
|
||||
Tensor::from_slice(&[30i64]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
|
||||
tc.evict_device_start(MAMBA, /* request_cnt = */ 1);
|
||||
let (next, _) = tc.evict_device_next_node(MAMBA, &HashMap::new());
|
||||
@@ -1220,7 +1263,8 @@ fn mamba_device_eviction_skips_a_load_back_pinned_node() {
|
||||
tc.evict_device_end(MAMBA);
|
||||
assert!(tc.arena.has_device_value(n, MAMBA));
|
||||
|
||||
tc.finish_load_back(tc.arena.node(n).id);
|
||||
tc.finish_load_back(tc.arena.node(n).id)
|
||||
.expect("live test node");
|
||||
tc.evict_device_start(MAMBA, /* request_cnt = */ 1);
|
||||
let (next, _) = tc.evict_device_next_node(MAMBA, &HashMap::new());
|
||||
assert_eq!(next, Some(tc.arena.node(n).id));
|
||||
@@ -1236,21 +1280,25 @@ fn mamba_host_eviction_skips_a_load_back_pinned_node() {
|
||||
set_full_host(&mut tc, b, 11);
|
||||
set_mamba_host(&mut tc, a, 20);
|
||||
tc.host_lru_list_mut(MAMBA).insert_mru(a);
|
||||
let (kv_xfer, comp_xfers) = tc.build_load_back_spec(tc.arena.node(b).id, /* req = */ None);
|
||||
let (kv_xfer, comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(b).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
assert!(comp_xfers.is_empty());
|
||||
tc.commit_load_back(
|
||||
tc.arena.node(b).id,
|
||||
Tensor::from_slice(&[30i64, 31]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
|
||||
let result = tc.drive_host_eviction(MAMBA, /* num_tokens = */ 1);
|
||||
assert_eq!(result.tracker[&MAMBA], 0);
|
||||
assert!(result.host_frees.is_empty());
|
||||
assert!(tc.arena.has_host_value(a, MAMBA));
|
||||
|
||||
tc.finish_load_back(tc.arena.node(b).id);
|
||||
tc.finish_load_back(tc.arena.node(b).id)
|
||||
.expect("live test node");
|
||||
let result = tc.drive_host_eviction(MAMBA, /* num_tokens = */ 1);
|
||||
assert_eq!(result.tracker[&MAMBA], 1);
|
||||
assert_eq!(result.host_frees[&MAMBA].len(), 1);
|
||||
@@ -1278,7 +1326,8 @@ fn backup_storage_commit_is_a_noop() {
|
||||
&mut cache_actions,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(tc.arena.node(a).has_host_value(MAMBA));
|
||||
assert!(cache_actions.is_empty());
|
||||
}
|
||||
@@ -1298,6 +1347,7 @@ fn backup_storage_build_keys_the_trailing_hash() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.is_none()
|
||||
);
|
||||
set_mamba_host(&mut tc, a, 8);
|
||||
@@ -1312,6 +1362,7 @@ fn backup_storage_build_keys_the_trailing_hash() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.is_none()
|
||||
);
|
||||
tc.arena.node_mut(a).hash_value = Some(vec!["h0".to_string(), "h1".to_string()]);
|
||||
@@ -1325,6 +1376,7 @@ fn backup_storage_build_keys_the_trailing_hash() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.unwrap();
|
||||
assert_eq!(transfers.len(), 1);
|
||||
assert_eq!(transfers[0].keys, Some(vec!["h1".to_string()]));
|
||||
@@ -1351,6 +1403,7 @@ fn prefetch_build_wraps_the_host_buffer_with_a_placeholder_key() {
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.expect("live test node")
|
||||
.unwrap();
|
||||
assert_eq!(transfers.len(), 1);
|
||||
assert_eq!(transfers[0].keys, Some(vec!["__placeholder__".to_string()]));
|
||||
@@ -1370,6 +1423,7 @@ fn prefetch_commit_attaches_the_loaded_slot_to_the_inserted_node() {
|
||||
Tensor::from_slice(&[100i64]),
|
||||
vec!["h0".to_string()],
|
||||
)
|
||||
.expect("live test node")
|
||||
.inserted_host_node
|
||||
.unwrap();
|
||||
let mut insert_result = InsertResult {
|
||||
@@ -1395,17 +1449,18 @@ fn prefetch_commit_attaches_the_loaded_slot_to_the_inserted_node() {
|
||||
kv_hit_pages: 1,
|
||||
extra_pool_hit_pages: HashMap::from([(PoolName::Mamba, 1)]),
|
||||
}),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(tc.arena.resolve(target))
|
||||
.node(tc.arena.resolve(target).expect("live test node"))
|
||||
.try_host_value(MAMBA)
|
||||
.unwrap()
|
||||
.equal(&Tensor::from_slice(&[50i64]))
|
||||
);
|
||||
assert!(
|
||||
tc.host_lru_list(MAMBA)
|
||||
.in_list(Some(tc.arena.resolve(target)))
|
||||
.in_list(Some(tc.arena.resolve(target).expect("live test node")))
|
||||
);
|
||||
assert!(!insert_result.mamba_exist);
|
||||
assert!(cache_actions.is_empty());
|
||||
@@ -1424,6 +1479,7 @@ fn prefetch_commit_frees_the_buffer_when_it_cannot_attach() {
|
||||
Tensor::from_slice(&[100i64]),
|
||||
vec!["h0".to_string()],
|
||||
)
|
||||
.expect("live test node")
|
||||
.inserted_host_node
|
||||
.unwrap();
|
||||
// Not loaded: the buffer frees and the caller keeps its slot flag.
|
||||
@@ -1450,10 +1506,11 @@ fn prefetch_commit_frees_the_buffer_when_it_cannot_attach() {
|
||||
kv_hit_pages: 1,
|
||||
extra_pool_hit_pages: HashMap::new(),
|
||||
}),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(
|
||||
!tc.arena
|
||||
.node(tc.arena.resolve(target))
|
||||
.node(tc.arena.resolve(target).expect("live test node"))
|
||||
.has_host_value(MAMBA)
|
||||
);
|
||||
assert!(insert_result.mamba_exist);
|
||||
@@ -1468,7 +1525,7 @@ fn prefetch_commit_frees_the_buffer_when_it_cannot_attach() {
|
||||
assert!(host_indices[0].equal(&Tensor::from_slice(&[50i64])));
|
||||
|
||||
// An already-hosted target frees the buffer too.
|
||||
let target_idx = tc.arena.resolve(target);
|
||||
let target_idx = tc.arena.resolve(target).expect("live test node");
|
||||
set_mamba_host(&mut tc, target_idx, 8);
|
||||
let mut insert_result = InsertResult {
|
||||
total_len: 1,
|
||||
@@ -1493,12 +1550,13 @@ fn prefetch_commit_frees_the_buffer_when_it_cannot_attach() {
|
||||
kv_hit_pages: 1,
|
||||
extra_pool_hit_pages: HashMap::from([(PoolName::Mamba, 1)]),
|
||||
}),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(insert_result.mamba_exist);
|
||||
assert_eq!(cache_actions.len(), 1);
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(tc.arena.resolve(target))
|
||||
.node(tc.arena.resolve(target).expect("live test node"))
|
||||
.try_host_value(MAMBA)
|
||||
.unwrap()
|
||||
.equal(&Tensor::from_slice(&[8i64]))
|
||||
@@ -1528,7 +1586,9 @@ fn evict_excess_path_states_removes_the_shallowest_states_beyond_the_cap() {
|
||||
set_mamba_device(&mut tc, a, 7);
|
||||
set_mamba_device(&mut tc, b, 8);
|
||||
set_mamba_device(&mut tc, c, 9);
|
||||
let mut result = tc.evict_excess_path_states(tc.arena.node(c).id);
|
||||
let mut result = tc
|
||||
.evict_excess_path_states(tc.arena.node(c).id)
|
||||
.expect("live test node");
|
||||
let freed = result
|
||||
.device_frees
|
||||
.remove(&MAMBA)
|
||||
@@ -1560,7 +1620,9 @@ fn evict_excess_path_states_preserves_forks_locked_nodes_and_the_tail() {
|
||||
tc.arena
|
||||
.node_mut(b)
|
||||
.set_lock_ref_(ValueSlotIdx::device(MAMBA), 1);
|
||||
let result = tc.evict_excess_path_states(tc.arena.node(c).id);
|
||||
let result = tc
|
||||
.evict_excess_path_states(tc.arena.node(c).id)
|
||||
.expect("live test node");
|
||||
assert!(result.device_frees.is_empty());
|
||||
assert!(result.host_frees.is_empty());
|
||||
assert!(tc.arena.node(a).try_device_value(MAMBA).is_some());
|
||||
@@ -1574,7 +1636,9 @@ fn evict_excess_path_states_without_a_cap_is_a_no_op() {
|
||||
let [a, b] = chain::<2>(&mut tc);
|
||||
set_mamba_device(&mut tc, a, 7);
|
||||
set_mamba_device(&mut tc, b, 8);
|
||||
let result = tc.evict_excess_path_states(tc.arena.node(b).id);
|
||||
let result = tc
|
||||
.evict_excess_path_states(tc.arena.node(b).id)
|
||||
.expect("live test node");
|
||||
assert!(result.device_frees.is_empty());
|
||||
assert!(result.host_frees.is_empty());
|
||||
assert!(tc.arena.node(a).try_device_value(MAMBA).is_some());
|
||||
@@ -1616,7 +1680,8 @@ fn swa_evict_on_a_full_locked_leaf_sweeps_mamba_and_spares_full() {
|
||||
let [a] = chain::<1>(&mut tc);
|
||||
tc.arena
|
||||
.set_device_value(a, FULL, Tensor::from_slice(&[10i64]));
|
||||
tc.set_component_device_value(tc.arena.node(a).id, SWA, Tensor::from_slice(&[20i64]));
|
||||
tc.set_component_device_value(tc.arena.node(a).id, SWA, Tensor::from_slice(&[20i64]))
|
||||
.expect("live test node");
|
||||
set_mamba_device(&mut tc, a, 7);
|
||||
// The held Full lock keeps the leaf out of the D-leaf set.
|
||||
tc.arena
|
||||
@@ -1721,11 +1786,20 @@ fn branching_from_a_host_full_hit_is_reusable_after_insert() {
|
||||
b,
|
||||
Tensor::from_slice(&[100i64, 101, 102, 103]),
|
||||
HashMap::new(),
|
||||
);
|
||||
tc.demote(b);
|
||||
)
|
||||
.expect("live test node");
|
||||
tc.demote(b).expect("valid demote state");
|
||||
// The demote's cascade swept b's mamba slot: b is Full-host-only, no mamba.
|
||||
assert!(!tc.arena.node(tc.arena.resolve(b)).has_device_value(MAMBA));
|
||||
assert!(!tc.arena.node(tc.arena.resolve(b)).has_host_value(MAMBA));
|
||||
assert!(
|
||||
!tc.arena
|
||||
.node(tc.arena.resolve(b).expect("live test node"))
|
||||
.has_device_value(MAMBA)
|
||||
);
|
||||
assert!(
|
||||
!tc.arena
|
||||
.node(tc.arena.resolve(b).expect("live test node"))
|
||||
.has_host_value(MAMBA)
|
||||
);
|
||||
let result = tc.match_prefix(&match_params(&vec![1, 2, 3, 4, 5, 6, 7]));
|
||||
assert_eq!(result.best_match_node_id, a);
|
||||
assert_eq!(result.last_device_node_id, a);
|
||||
|
||||
@@ -172,7 +172,8 @@ fn store_swa_device(tc: &mut UnifiedTreeCore<Vec<i64>>, node: NodeIdx_) {
|
||||
tc.arena.node(node).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&vec![0i64; len]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -393,6 +394,7 @@ fn finalize(
|
||||
best: NodeIdx_,
|
||||
prior_swa_host_hit: usize,
|
||||
) -> MatchResult {
|
||||
let last_device_node_idx = tc.arena.root();
|
||||
swa.finalize_match_result_in_tree_core(
|
||||
tc,
|
||||
MatchResult {
|
||||
@@ -400,6 +402,8 @@ fn finalize(
|
||||
swa_host_hit_length: prior_swa_host_hit,
|
||||
..tc.empty_match_result()
|
||||
},
|
||||
last_device_node_idx,
|
||||
best,
|
||||
&MatchPrefixParams {
|
||||
key: &Vec::new(),
|
||||
namespace: Default::default(),
|
||||
@@ -704,7 +708,8 @@ fn insert_overlap_with_live_swa_frees_the_whole_duplicate() {
|
||||
tc.arena.node(leaf).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&[50i64, 51, 52]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
let result = tc.insert(&insert_params_swa(&vec![1, 2, 3], &[20, 21, 22], 0, 0));
|
||||
assert_eq!(result.prefix_len, 3);
|
||||
let [CacheAction::FreeDeviceKV(freed)] = result.cache_actions.as_slice() else {
|
||||
@@ -957,7 +962,8 @@ fn insert_overlap_boundary_at_the_node_start_recovers_the_whole_node() {
|
||||
tc.arena.node(a).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&[50i64, 51, 52]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
// The boundary lands exactly on b's start: full recovery, no split.
|
||||
let result = tc.insert(&insert_params_swa(
|
||||
&vec![1, 2, 3, 4, 5],
|
||||
@@ -1285,7 +1291,8 @@ fn reinsert_with_live_swa_skips_recovery() {
|
||||
tc.arena.node(leaf).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&[50i64, 51, 52]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
evict_full(&mut tc, leaf, /* remaining_size = */ 0);
|
||||
let result = tc.insert(&insert_params_swa(&vec![1, 2, 3], &[20, 21, 22], 0, 0));
|
||||
// The SWA value is already live: no rebuild is emitted.
|
||||
@@ -1353,7 +1360,8 @@ fn walk_split_redistributes_the_live_swa_value() {
|
||||
tc.arena.node(node).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&[50i64, 51, 52, 53]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
let result = tc.insert(&insert_params_swa(&vec![1, 2, 9], &[20, 21, 29], 0, 0));
|
||||
assert_eq!(result.prefix_len, 2);
|
||||
let parent = child_of(&tc, root, &[1]);
|
||||
@@ -1424,7 +1432,8 @@ fn redistribute_on_node_split_keeps_device_valued_sides_off_the_host_lru() {
|
||||
tc.arena.node(node).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&[50i64, 51]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
tc.arena
|
||||
.set_host_value(node, SWA, Tensor::from_slice(&[70i64, 71]));
|
||||
tc.arena
|
||||
@@ -1614,7 +1623,9 @@ fn inc_lock_ref_runs_full_and_swa_walks_together() {
|
||||
store_swa_device(&mut tc, a);
|
||||
store_swa_device(&mut tc, b);
|
||||
store_swa_device(&mut tc, c);
|
||||
let result = tc.inc_lock_ref(tc.arena.node(c).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(c).id)
|
||||
.expect("live test node");
|
||||
// FULL sees a valueless path (skip segment only); SWA locks its window.
|
||||
assert_eq!(result.delta, Some(0));
|
||||
assert_eq!(result.skip_lock_node_ids[&FULL].len(), 3);
|
||||
@@ -1633,7 +1644,9 @@ fn inc_host_lock_ref_runs_full_and_swa_host_arms_together() {
|
||||
}
|
||||
tc.arena
|
||||
.set_host_value(c, FULL, Tensor::from_slice(&[0i64]));
|
||||
let result = tc.inc_host_lock_ref(tc.arena.node(c).id);
|
||||
let result = tc
|
||||
.inc_host_lock_ref(tc.arena.node(c).id)
|
||||
.expect("live test node");
|
||||
// FULL pins only the anchor; SWA walks its host window up to b.
|
||||
assert_eq!(tc.arena.host_lock_ref(c, FULL), 1);
|
||||
assert_eq!(tc.arena.host_lock_ref(b, FULL), 0);
|
||||
@@ -1647,7 +1660,8 @@ fn inc_host_lock_ref_runs_full_and_swa_host_arms_together() {
|
||||
skip_lock_node_ids: result.skip_lock_node_ids,
|
||||
..Default::default()
|
||||
};
|
||||
tc.dec_host_lock_ref(tc.arena.node(c).id, Some(¶ms));
|
||||
tc.dec_host_lock_ref(tc.arena.node(c).id, Some(¶ms))
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(c, FULL), 0);
|
||||
assert_eq!(tc.arena.host_lock_ref(c, SWA), 0);
|
||||
assert_eq!(tc.arena.host_lock_ref(b, SWA), 0);
|
||||
@@ -1663,8 +1677,11 @@ fn dec_host_lock_ref_with_the_inner_uuid_leaves_an_outer_window_pinned() {
|
||||
set_swa_host(&mut tc, node);
|
||||
}
|
||||
// Overlapping host windows: {c, b} stamps its uuid at b, {b, a} at a.
|
||||
let inner = tc.inc_host_lock_ref(tc.arena.node(c).id);
|
||||
tc.inc_host_lock_ref(tc.arena.node(b).id);
|
||||
let inner = tc
|
||||
.inc_host_lock_ref(tc.arena.node(c).id)
|
||||
.expect("live test node");
|
||||
tc.inc_host_lock_ref(tc.arena.node(b).id)
|
||||
.expect("live test node");
|
||||
assert!(inner.swa_uuid_for_host_lock.is_some());
|
||||
assert_eq!(tc.arena.host_lock_ref(c, SWA), 1);
|
||||
assert_eq!(tc.arena.host_lock_ref(b, SWA), 2);
|
||||
@@ -1676,7 +1693,8 @@ fn dec_host_lock_ref_with_the_inner_uuid_leaves_an_outer_window_pinned() {
|
||||
skip_lock_node_ids: inner.skip_lock_node_ids,
|
||||
..Default::default()
|
||||
};
|
||||
tc.dec_host_lock_ref(tc.arena.node(c).id, Some(¶ms));
|
||||
tc.dec_host_lock_ref(tc.arena.node(c).id, Some(¶ms))
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.host_lock_ref(c, SWA), 0);
|
||||
assert_eq!(tc.arena.host_lock_ref(b, SWA), 1);
|
||||
assert_eq!(tc.arena.host_lock_ref(a, SWA), 1);
|
||||
@@ -1888,7 +1906,8 @@ fn evict_component_device_frees_the_full_indices_and_tombstones_swa() {
|
||||
tc.arena.node(node).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&[50i64, 51]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
let mut device_frees = HashMap::new();
|
||||
let mut host_frees = HashMap::new();
|
||||
let (freed, host_freed) = swa_component(4).evict_component(
|
||||
@@ -1921,7 +1940,8 @@ fn evict_component_device_parks_a_remaining_host_value() {
|
||||
tc.arena.node(node).id,
|
||||
SWA,
|
||||
Tensor::from_slice(&[50i64, 51]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
set_swa_host(&mut tc, node);
|
||||
let mut device_frees = HashMap::new();
|
||||
let mut host_frees = HashMap::new();
|
||||
@@ -2177,7 +2197,9 @@ fn inc_then_dec_lock_ref_roundtrips_with_dec_params() {
|
||||
store_swa_device(&mut tc, a);
|
||||
store_swa_device(&mut tc, b);
|
||||
store_swa_device(&mut tc, c);
|
||||
let result = tc.inc_lock_ref(tc.arena.node(c).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(c).id)
|
||||
.expect("live test node");
|
||||
let params = DecLockRefParams {
|
||||
swa_uuid_for_lock: result.swa_uuid_for_lock,
|
||||
swa_uuid_for_host_lock: result.swa_uuid_for_host_lock,
|
||||
@@ -2187,7 +2209,8 @@ fn inc_then_dec_lock_ref_roundtrips_with_dec_params() {
|
||||
tc.arena.node(c).id,
|
||||
Some(¶ms),
|
||||
/* skip_swa = */ false,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(c, SWA), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(b, SWA), 0);
|
||||
assert_eq!(tc.swa_evictable_size(), 3);
|
||||
@@ -2206,7 +2229,9 @@ fn dec_swa_lock_only_releases_swa_while_full_stays_locked() {
|
||||
}
|
||||
// Fund FULL's evictable counter for its lock walk (raw slot sets skip it).
|
||||
tc.component_state_mut(FULL).evictable_size = 3;
|
||||
let result = tc.inc_lock_ref(tc.arena.node(c).id);
|
||||
let result = tc
|
||||
.inc_lock_ref(tc.arena.node(c).id)
|
||||
.expect("live test node");
|
||||
let mut device_frees = HashMap::new();
|
||||
let mut host_frees = HashMap::new();
|
||||
tc.dec_swa_lock_only(
|
||||
@@ -2214,7 +2239,8 @@ fn dec_swa_lock_only_releases_swa_while_full_stays_locked() {
|
||||
result.swa_uuid_for_lock,
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
// SWA is early-released; the FULL locks on the path stay.
|
||||
assert_eq!(tc.arena.device_lock_ref(c, SWA), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(b, SWA), 0);
|
||||
@@ -2250,7 +2276,8 @@ fn dec_swa_lock_only_evicts_a_fully_unlocked_device_leaf() {
|
||||
result.swa_uuid_for_lock,
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
// The fully unlocked leaf c is device-evicted on release; b keeps its
|
||||
// SWA value because its child still holds FULL KV.
|
||||
assert!(!tc.arena.has_device_value(c, SWA));
|
||||
@@ -2272,7 +2299,8 @@ fn dec_swa_lock_only_is_a_noop_without_the_swa_component() {
|
||||
None,
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(device_frees.is_empty());
|
||||
}
|
||||
|
||||
@@ -2410,7 +2438,8 @@ fn dec_swa_lock_only_releases_the_window_exactly_once() {
|
||||
first.swa_uuid_for_lock,
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
// The second window still holds the lock: refs drop to 1, sizes stay.
|
||||
assert_eq!(tc.arena.device_lock_ref(c, SWA), 1);
|
||||
assert_eq!(tc.arena.device_lock_ref(b, SWA), 1);
|
||||
@@ -2421,7 +2450,8 @@ fn dec_swa_lock_only_releases_the_window_exactly_once() {
|
||||
first.swa_uuid_for_lock,
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.device_lock_ref(c, SWA), 0);
|
||||
assert_eq!(tc.arena.device_lock_ref(b, SWA), 0);
|
||||
assert_eq!(tc.swa_evictable_size(), 3);
|
||||
@@ -2456,7 +2486,8 @@ fn dec_swa_lock_only_leaves_out_of_window_swa_locks_alone() {
|
||||
result.swa_uuid_for_lock,
|
||||
&mut device_frees,
|
||||
&mut host_frees,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
// Only the SWA window is released; a's out-of-window lock survives.
|
||||
assert_eq!(tc.arena.device_lock_ref(a, SWA), 1);
|
||||
assert_eq!(tc.arena.device_lock_ref(b, SWA), 0);
|
||||
@@ -2835,11 +2866,13 @@ fn try_device_value_and_evictable_size_read_the_swa_slots() {
|
||||
store_swa_device(&mut tc, a);
|
||||
assert!(
|
||||
tc.get_component_device_value(tc.arena.node(a).id, SWA)
|
||||
.expect("live test node")
|
||||
.unwrap()
|
||||
.equal(&Tensor::from_slice(&[0i64]))
|
||||
);
|
||||
assert!(
|
||||
tc.get_component_device_value(tc.arena.node(b).id, SWA)
|
||||
.expect("live test node")
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(tc.evictable_size_(SWA), 1);
|
||||
@@ -3620,12 +3653,13 @@ fn backup_spec_reads_the_swa_value_recovered_by_an_earlier_action() {
|
||||
.set_device_value(a, FULL, Tensor::from_slice(&[9i64]));
|
||||
let a_id = tc.arena.node(a).id;
|
||||
// The tombstone carries no SWA transfer into the backup spec.
|
||||
let (_, xfers) = tc.build_backup_spec(a_id);
|
||||
let (_, xfers) = tc.build_backup_spec(a_id).expect("live test node");
|
||||
assert!(xfers.is_empty());
|
||||
// The cache resolves the recover/rebuild action, then rebuilds the spec:
|
||||
// the deferred read now captures the freshly stored SWA value.
|
||||
tc.set_component_device_value(a_id, SWA, Tensor::from_slice(&[50i64]));
|
||||
let (_, xfers) = tc.build_backup_spec(a_id);
|
||||
tc.set_component_device_value(a_id, SWA, Tensor::from_slice(&[50i64]))
|
||||
.expect("live test node");
|
||||
let (_, xfers) = tc.build_backup_spec(a_id).expect("live test node");
|
||||
let swa_xfer = &xfers[&SWA][0];
|
||||
assert!(
|
||||
swa_xfer
|
||||
@@ -3762,7 +3796,7 @@ fn fallible_load_back_boundaries_reject_a_bare_window_node() {
|
||||
let node_id = tc.arena.node(a).id;
|
||||
|
||||
assert!(matches!(
|
||||
tc.try_build_hicache_transfers(
|
||||
tc.build_hicache_transfers(
|
||||
SWA,
|
||||
node_id,
|
||||
CacheTransferPhase::LoadBack,
|
||||
@@ -3775,7 +3809,7 @@ fn fallible_load_back_boundaries_reject_a_bare_window_node() {
|
||||
if missing == node_id
|
||||
));
|
||||
assert!(matches!(
|
||||
tc.try_build_load_back_spec(node_id, /* req = */ None),
|
||||
tc.build_load_back_spec(node_id, /* req = */ None),
|
||||
Err(TreeCoreRuntimeError::SwaLoadBackMissingValue { node_id: missing })
|
||||
if missing == node_id
|
||||
));
|
||||
@@ -3974,7 +4008,8 @@ fn commit_hicache_transfers_routes_to_the_component() {
|
||||
&mut cache_actions,
|
||||
/* insert_result = */ None,
|
||||
/* pool_storage_result = */ None,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(cache_actions.len(), 1);
|
||||
}
|
||||
|
||||
@@ -4109,9 +4144,9 @@ fn prefetch_commit_without_a_target_releases_the_whole_buffer() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_commit_releases_the_out_of_path_prefix() {
|
||||
fn prefetch_commit_releases_a_shortened_window_under_a_non_root_anchor() {
|
||||
// root -> a -> b -> c, one token each; anchor b, target c: the loaded
|
||||
// window spans two tokens but the leaf->anchor path covers only c's one.
|
||||
// window is missing its head and cannot be reused as a complete SWA window.
|
||||
let mut tc = swa_core(/* window = */ 4, /* page_size = */ 1);
|
||||
let [a, b, c] = chain::<3>(&mut tc);
|
||||
let mut cache_actions = Vec::new();
|
||||
@@ -4137,21 +4172,15 @@ fn prefetch_commit_releases_the_out_of_path_prefix() {
|
||||
Some(&mut insert_result),
|
||||
Some(&storage_result),
|
||||
);
|
||||
// c (on path) fills with the buffer tail; the out-of-path prefix releases.
|
||||
assert!(
|
||||
tc.arena
|
||||
.node(c)
|
||||
.host_value(SWA)
|
||||
.equal(&Tensor::from_slice(&[31i64]))
|
||||
);
|
||||
assert!(!tc.arena.node(c).has_host_value(SWA));
|
||||
assert!(!tc.arena.node(b).has_host_value(SWA));
|
||||
assert!(!tc.arena.node(a).has_host_value(SWA));
|
||||
assert!(tc.host_lru_list(SWA).in_list(Some(c)));
|
||||
assert!(!tc.host_lru_list(SWA).in_list(Some(c)));
|
||||
assert_eq!(cache_actions.len(), 1);
|
||||
let CacheAction::FreeComponentHostSlot { host_indices, .. } = &cache_actions[0] else {
|
||||
panic!("expected a host free");
|
||||
};
|
||||
assert!(host_indices[0].equal(&Tensor::from_slice(&[30i64])));
|
||||
assert!(host_indices[0].equal(&Tensor::from_slice(&[30i64, 31])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4317,8 +4346,9 @@ fn build_load_back_spec_includes_the_swa_transfers() {
|
||||
set_full_host(&mut tc, n);
|
||||
tc.arena
|
||||
.set_host_value(n, SWA, Tensor::from_slice(&[30i64]));
|
||||
let (kv_xfer, mut comp_xfers) =
|
||||
tc.build_load_back_spec(tc.arena.node(n).id, /* req = */ None);
|
||||
let (kv_xfer, mut comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(n).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(kv_xfer.nodes_to_load, Some(vec![tc.arena.node(n).id]));
|
||||
let swa_xfers = comp_xfers.get_mut(&SWA).unwrap();
|
||||
assert_eq!(swa_xfers.len(), 1);
|
||||
@@ -4332,12 +4362,14 @@ fn build_load_back_spec_includes_the_swa_transfers() {
|
||||
assert_eq!(swa_xfers[0].nodes_to_load, Some(vec![tc.arena.node(n).id]));
|
||||
// The orchestrator fills each transfer's device side from the pool load.
|
||||
swa_xfers[0].device_indices = Some(Tensor::from_slice(&[60i64]));
|
||||
let actions = tc.commit_load_back(
|
||||
tc.arena.node(n).id,
|
||||
Tensor::from_slice(&[50i64]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
);
|
||||
let actions = tc
|
||||
.commit_load_back(
|
||||
tc.arena.node(n).id,
|
||||
Tensor::from_slice(&[50i64]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(
|
||||
tc.arena
|
||||
.device_value(n, FULL)
|
||||
@@ -4383,7 +4415,8 @@ fn auxiliary_load_does_not_reuse_a_full_pending_pin() {
|
||||
..Default::default()
|
||||
},
|
||||
HashMap::new(),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
assert_eq!(tc.arena.node(shared).load_back_pending_id, Some(shared_id));
|
||||
|
||||
let swa_xfer = PoolTransfer {
|
||||
@@ -4403,7 +4436,8 @@ fn auxiliary_load_does_not_reuse_a_full_pending_pin() {
|
||||
..Default::default()
|
||||
},
|
||||
HashMap::from([(SWA, vec![swa_xfer])]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
|
||||
assert_eq!(tc.arena.node(shared).load_back_pending_id, Some(shared_id));
|
||||
assert_eq!(
|
||||
@@ -4426,22 +4460,25 @@ fn swa_device_eviction_skips_a_load_back_pinned_node() {
|
||||
let [n] = chain::<1>(&mut tc);
|
||||
set_full_host(&mut tc, n);
|
||||
set_swa_host(&mut tc, n);
|
||||
let (kv_xfer, mut comp_xfers) =
|
||||
tc.build_load_back_spec(tc.arena.node(n).id, /* req = */ None);
|
||||
let (kv_xfer, mut comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(n).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
comp_xfers.get_mut(&SWA).unwrap()[0].device_indices = Some(Tensor::from_slice(&[60i64]));
|
||||
tc.commit_load_back(
|
||||
tc.arena.node(n).id,
|
||||
Tensor::from_slice(&[50i64]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
// The pin alone keeps the in-flight SWA slice out of every eviction branch.
|
||||
tc.evict_device_start(SWA, 4);
|
||||
let (next, _) = tc.evict_device_next_node(SWA, &HashMap::new());
|
||||
assert_eq!(next, None);
|
||||
tc.evict_device_end(SWA);
|
||||
assert!(tc.arena.has_device_value(n, SWA));
|
||||
tc.finish_load_back(tc.arena.node(n).id);
|
||||
tc.finish_load_back(tc.arena.node(n).id)
|
||||
.expect("live test node");
|
||||
tc.evict_device_start(SWA, 4);
|
||||
let (next, _) = tc.evict_device_next_node(SWA, &HashMap::new());
|
||||
assert_eq!(next, Some(tc.arena.node(n).id));
|
||||
@@ -4465,8 +4502,9 @@ fn swa_host_eviction_skips_a_load_back_pinned_node() {
|
||||
set_full_host(&mut tc, b);
|
||||
set_swa_host(&mut tc, b);
|
||||
tc.host_lru_list_mut(SWA).insert_mru(a);
|
||||
let (kv_xfer, mut comp_xfers) =
|
||||
tc.build_load_back_spec(tc.arena.node(b).id, /* req = */ None);
|
||||
let (kv_xfer, mut comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(b).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(
|
||||
comp_xfers.get(&SWA).unwrap()[0].nodes_to_load,
|
||||
Some(vec![tc.arena.node(b).id])
|
||||
@@ -4477,14 +4515,16 @@ fn swa_host_eviction_skips_a_load_back_pinned_node() {
|
||||
Tensor::from_slice(&[50i64, 51]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
|
||||
let result = tc.drive_host_eviction(SWA, /* num_tokens = */ 1);
|
||||
assert_eq!(result.tracker[&SWA], 0);
|
||||
assert!(result.host_frees.is_empty());
|
||||
assert!(tc.arena.has_host_value(a, SWA));
|
||||
|
||||
tc.finish_load_back(tc.arena.node(b).id);
|
||||
tc.finish_load_back(tc.arena.node(b).id)
|
||||
.expect("live test node");
|
||||
let result = tc.drive_host_eviction(SWA, /* num_tokens = */ 1);
|
||||
assert_eq!(result.tracker[&SWA], 1);
|
||||
assert_eq!(result.host_frees[&SWA].len(), 1);
|
||||
@@ -4511,21 +4551,28 @@ fn build_load_back_spec_degrades_to_empty_on_a_foreign_pin() {
|
||||
set_full_host(&mut tc, b);
|
||||
set_swa_host(&mut tc, b);
|
||||
// Anchor `a` models a Full-only load whose SWA slice remains host-only.
|
||||
let (kv_xfer, _comp_xfers) =
|
||||
tc.build_load_back_spec(tc.arena.node(a).id, /* req = */ None);
|
||||
let (kv_xfer, _comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(a).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
tc.commit_load_back(
|
||||
tc.arena.node(a).id,
|
||||
Tensor::from_slice(&[50i64]),
|
||||
kv_xfer,
|
||||
HashMap::new(),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
// Anchor `b` must reject its SWA window because `a` has a foreign pin.
|
||||
let (kv_xfer, comp_xfers) = tc.build_load_back_spec(tc.arena.node(b).id, /* req = */ None);
|
||||
let (kv_xfer, comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(b).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(kv_xfer.host_indices.unwrap().numel(), 0);
|
||||
assert_eq!(kv_xfer.nodes_to_load, Some(vec![]));
|
||||
assert!(comp_xfers.is_empty());
|
||||
tc.finish_load_back(tc.arena.node(a).id);
|
||||
let (kv_xfer, comp_xfers) = tc.build_load_back_spec(tc.arena.node(b).id, /* req = */ None);
|
||||
tc.finish_load_back(tc.arena.node(a).id)
|
||||
.expect("live test node");
|
||||
let (kv_xfer, comp_xfers) = tc
|
||||
.build_load_back_spec(tc.arena.node(b).id, /* req = */ None)
|
||||
.expect("live test node");
|
||||
assert_eq!(kv_xfer.nodes_to_load, Some(vec![tc.arena.node(b).id]));
|
||||
assert_eq!(
|
||||
comp_xfers.get(&SWA).unwrap()[0].nodes_to_load,
|
||||
@@ -4551,7 +4598,8 @@ fn host_drive_reclaims_swa_coexisting_host_values_when_the_host_lru_is_empty() {
|
||||
let leaf_idx = child_of(&tc, parent_idx, &[3]);
|
||||
let (parent, leaf) = (tc.arena.node(parent_idx).id, tc.arena.node(leaf_idx).id);
|
||||
for (handle, slots) in [(parent, vec![30i64, 31]), (leaf, vec![32i64])] {
|
||||
tc.set_component_device_value(handle, SWA, Tensor::from_slice(&slots));
|
||||
tc.set_component_device_value(handle, SWA, Tensor::from_slice(&slots))
|
||||
.expect("live test node");
|
||||
}
|
||||
for (handle, host) in [(parent, vec![20i64, 21]), (leaf, vec![22i64])] {
|
||||
let swa_xfer = PoolTransfer {
|
||||
@@ -4563,7 +4611,8 @@ fn host_drive_reclaims_swa_coexisting_host_values_when_the_host_lru_is_empty() {
|
||||
handle,
|
||||
Tensor::from_slice(&host),
|
||||
HashMap::from([(SWA, vec![swa_xfer])]),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
}
|
||||
assert_eq!(tc.host_lru_list(SWA).len(), 0);
|
||||
|
||||
@@ -4667,12 +4716,14 @@ fn write_through_offloads_a_boundary_split_leaf() {
|
||||
tc.arena.node(parent).id,
|
||||
Tensor::from_slice(&[100i64, 101]),
|
||||
HashMap::new(),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
tc.commit_backup(
|
||||
tc.arena.node(leaf).id,
|
||||
Tensor::from_slice(&[102i64, 103]),
|
||||
HashMap::new(),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
let mut tracker = swa_tracker();
|
||||
let (mut df, mut hf) = (HashMap::new(), HashMap::new());
|
||||
tc.evict_device_start(FULL, /* request_cnt = */ 100);
|
||||
@@ -4680,7 +4731,9 @@ fn write_through_offloads_a_boundary_split_leaf() {
|
||||
let (next, step) = tc.evict_device_next_node(FULL, &tracker);
|
||||
accumulate_step(step, &mut tracker, &mut df, &mut hf);
|
||||
let Some(next) = next else { break };
|
||||
let (backup, step) = tc.evict_device_leaf(next, /* is_write_back = */ false);
|
||||
let (backup, step) = tc
|
||||
.evict_device_leaf(next, /* is_write_back = */ false)
|
||||
.expect("live test node");
|
||||
assert!(backup.is_none());
|
||||
accumulate_step(step, &mut tracker, &mut df, &mut hf);
|
||||
}
|
||||
@@ -4727,7 +4780,8 @@ fn deep_swa_tree_survives_backup_evict_and_load_back_rounds() {
|
||||
source_value,
|
||||
} = action
|
||||
{
|
||||
tc.set_component_device_value(*node_id, SWA, source_value.copy());
|
||||
tc.set_component_device_value(*node_id, SWA, source_value.copy())
|
||||
.expect("live test node");
|
||||
}
|
||||
}
|
||||
tc.sanity_check(&[], &[]);
|
||||
@@ -4744,7 +4798,8 @@ fn deep_swa_tree_survives_backup_evict_and_load_back_rounds() {
|
||||
tc.arena.node(node).id,
|
||||
Tensor::from_slice(&vec![0i64; len]),
|
||||
HashMap::new(),
|
||||
);
|
||||
)
|
||||
.expect("live test node");
|
||||
}
|
||||
tc.sanity_check(&[], &[]);
|
||||
// Stepwise eviction rounds: half the Full budget, then the whole SWA budget.
|
||||
@@ -4757,7 +4812,9 @@ fn deep_swa_tree_survives_backup_evict_and_load_back_rounds() {
|
||||
let (leaf, step) = tc.evict_device_next_node(FULL, &tracker);
|
||||
accumulate_step(step, &mut tracker, &mut df, &mut hf);
|
||||
let Some(leaf) = leaf else { break };
|
||||
let (_, step) = tc.evict_device_leaf(leaf, /* is_write_back = */ false);
|
||||
let (_, step) = tc
|
||||
.evict_device_leaf(leaf, /* is_write_back = */ false)
|
||||
.expect("live test node");
|
||||
accumulate_step(step, &mut tracker, &mut df, &mut hf);
|
||||
}
|
||||
tc.evict_device_end(FULL);
|
||||
@@ -4770,7 +4827,9 @@ fn deep_swa_tree_survives_backup_evict_and_load_back_rounds() {
|
||||
let (leaf, step) = tc.evict_device_next_node(SWA, &tracker);
|
||||
accumulate_step(step, &mut tracker, &mut df, &mut hf);
|
||||
let Some(leaf) = leaf else { break };
|
||||
let (_, step) = tc.evict_device_leaf(leaf, /* is_write_back = */ false);
|
||||
let (_, step) = tc
|
||||
.evict_device_leaf(leaf, /* is_write_back = */ false)
|
||||
.expect("live test node");
|
||||
accumulate_step(step, &mut tracker, &mut df, &mut hf);
|
||||
}
|
||||
tc.evict_device_end(SWA);
|
||||
@@ -4781,24 +4840,31 @@ fn deep_swa_tree_survives_backup_evict_and_load_back_rounds() {
|
||||
// commit-then-lock sequence.
|
||||
for key in [vec![1i64, 2, 3, 4, 5, 6], vec![1i64, 2]] {
|
||||
let anchor = tc.match_prefix(&match_params(&key)).best_match_node_id;
|
||||
if !tc.is_root(anchor) && tc.is_full_device_evicted(anchor) {
|
||||
let (kv_xfer, comp_xfers) = tc.build_load_back_spec(anchor, /* req = */ None);
|
||||
if !tc.is_root(anchor).expect("live test node")
|
||||
&& tc.is_full_device_evicted(anchor).expect("live test node")
|
||||
{
|
||||
let (kv_xfer, comp_xfers) = tc
|
||||
.build_load_back_spec(anchor, /* req = */ None)
|
||||
.expect("live test node");
|
||||
let loaded = kv_xfer.host_indices.as_ref().unwrap().numel();
|
||||
let actions = tc.commit_load_back(
|
||||
anchor,
|
||||
Tensor::from_slice(&vec![0i64; loaded]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
);
|
||||
let actions = tc
|
||||
.commit_load_back(
|
||||
anchor,
|
||||
Tensor::from_slice(&vec![0i64; loaded]),
|
||||
kv_xfer,
|
||||
comp_xfers,
|
||||
)
|
||||
.expect("live test node");
|
||||
assert!(actions.is_empty());
|
||||
let lock = tc.inc_lock_ref(anchor);
|
||||
let lock = tc.inc_lock_ref(anchor).expect("live test node");
|
||||
let params = DecLockRefParams {
|
||||
swa_uuid_for_lock: lock.swa_uuid_for_lock,
|
||||
swa_uuid_for_host_lock: lock.swa_uuid_for_host_lock,
|
||||
skip_lock_node_ids: lock.skip_lock_node_ids,
|
||||
};
|
||||
tc.dec_lock_ref(anchor, Some(¶ms), /* skip_swa = */ false);
|
||||
tc.finish_load_back(anchor);
|
||||
tc.dec_lock_ref(anchor, Some(¶ms), /* skip_swa = */ false)
|
||||
.expect("live test node");
|
||||
tc.finish_load_back(anchor).expect("live test node");
|
||||
}
|
||||
tc.sanity_check(&[], &[]);
|
||||
}
|
||||
@@ -4836,7 +4902,8 @@ fn recovered_swa_span_evicts_before_the_window_leaf() {
|
||||
);
|
||||
};
|
||||
assert_eq!(*node_id, tc.arena.node(leaf).id);
|
||||
tc.set_component_device_value(*node_id, SWA, source_value.copy());
|
||||
tc.set_component_device_value(*node_id, SWA, source_value.copy())
|
||||
.expect("live test node");
|
||||
assert!(!tc.arena.has_device_value(prefix, SWA));
|
||||
|
||||
// The fully-in-window re-insert recovers the prefix at its walk barrier.
|
||||
@@ -4866,7 +4933,8 @@ fn recovered_swa_span_evicts_before_the_window_leaf() {
|
||||
100i64, 101, 102, 103, 104, 105, 106, 107
|
||||
])));
|
||||
assert_eq!(*node_id, tc.arena.node(prefix).id);
|
||||
tc.set_component_device_value(*node_id, SWA, source_value.copy());
|
||||
tc.set_component_device_value(*node_id, SWA, source_value.copy())
|
||||
.expect("live test node");
|
||||
let done = tc.resume_insert();
|
||||
assert_eq!(
|
||||
done.result.expect("the resumed walk completes").prefix_len,
|
||||
|
||||
@@ -5,7 +5,7 @@ use tch::Tensor;
|
||||
|
||||
use super::*;
|
||||
use crate::components::{FULL, MAMBA, SWA};
|
||||
use crate::node::TreeCoreRuntimeError;
|
||||
use crate::node::{NodeAccessError, TreeCoreRuntimeError};
|
||||
|
||||
static COUNTED_KEY_CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
@@ -1264,7 +1264,7 @@ fn alloc_stamps_self_id_and_a_fresh_access_tick() -> Result<(), TreeCoreRuntimeE
|
||||
)?;
|
||||
assert_eq!(arena.node(a).id, 1);
|
||||
assert_eq!(arena.node(b).id, 2);
|
||||
assert_eq!(arena.resolve(arena.node(a).id), a);
|
||||
assert_eq!(arena.resolve(arena.node(a).id).expect("live test node"), a);
|
||||
// Construction stamps strictly increasing ticks: root, then a, then b;
|
||||
// both stamps share the node's single construction tick.
|
||||
let root_tick = arena.node(root).last_access_counter;
|
||||
@@ -1711,11 +1711,13 @@ fn failed_alloc_child_mints_no_id_and_keeps_the_freelist() -> Result<(), TreeCor
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "is not allocated")]
|
||||
fn resolve_panics_on_a_never_minted_handle() {
|
||||
fn resolve_returns_err_for_a_never_minted_handle() {
|
||||
let arena = arena();
|
||||
arena.root();
|
||||
arena.resolve(1_000_000);
|
||||
assert!(matches!(
|
||||
arena.resolve(1_000_000),
|
||||
Err(NodeAccessError { node_id: 1_000_000 })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1815,7 +1817,7 @@ fn id_map_stays_consistent_across_free_and_realloc() -> Result<(), TreeCoreRunti
|
||||
)?;
|
||||
let b_id = arena.node(b).id;
|
||||
arena.free_leaf(b)?;
|
||||
assert!(arena.try_resolve(b_id).is_none());
|
||||
assert!(arena.resolve(b_id).is_err());
|
||||
// The freed slot is recycled with a fresh handle; the old one stays dead.
|
||||
let c = arena.alloc_child(
|
||||
root,
|
||||
@@ -1825,10 +1827,13 @@ fn id_map_stays_consistent_across_free_and_realloc() -> Result<(), TreeCoreRunti
|
||||
)?;
|
||||
assert_eq!(c, b);
|
||||
assert_ne!(arena.node(c).id, b_id);
|
||||
assert!(arena.try_resolve(b_id).is_none());
|
||||
assert!(arena.resolve(b_id).is_err());
|
||||
// Every live slot resolves back from its own handle.
|
||||
for idx in arena.live_ids().collect::<Vec<_>>() {
|
||||
assert_eq!(arena.resolve(arena.node(idx).id), idx);
|
||||
assert_eq!(
|
||||
arena.resolve(arena.node(idx).id).expect("live test node"),
|
||||
idx
|
||||
);
|
||||
}
|
||||
let _ = a;
|
||||
Ok(())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
is_in_amd_ci,
|
||||
popen_launch_server,
|
||||
terminate_and_kill_process_tree,
|
||||
unified_radix_tree_server_env,
|
||||
@@ -49,6 +50,7 @@ class TestUnifiedFullRadixCache(UnifiedRadixTreeTestMixin, CustomTestCase):
|
||||
terminate_and_kill_process_tree(cls.process, wait_timeout=60)
|
||||
|
||||
|
||||
@unittest.skipIf(is_in_amd_ci(), "Rust TreeCore is not packaged in AMD CI")
|
||||
class TestRustUnifiedFullRadixCache(TestUnifiedFullRadixCache):
|
||||
tree_core_backend = "rust"
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ crate-type = ["cdylib"]
|
||||
self.assertEqual(crate.library, "demo_extension")
|
||||
self.assertEqual(crate.python_module, "demo._core")
|
||||
self.assertEqual(crate.features, ("python",))
|
||||
self.assertEqual(crate.source_inputs, ())
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ModuleNotFoundError, r"declared modules: \['demo\._core'\]"
|
||||
@@ -208,6 +209,36 @@ crate-type = ["cdylib"]
|
||||
inspection.target_fingerprint,
|
||||
)
|
||||
|
||||
def test_fingerprint_covers_declared_external_source_inputs(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
workspace = self._workspace(root)
|
||||
proto = root / "proto/demo.proto"
|
||||
proto.parent.mkdir()
|
||||
proto.write_text("message Demo {}\n", encoding="utf-8")
|
||||
manifest = workspace / "demo/Cargo.toml"
|
||||
manifest.write_text(
|
||||
manifest.read_text(encoding="utf-8").replace(
|
||||
'features = ["python"]',
|
||||
'features = ["python"]\nsource-inputs = ["../../proto"]',
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
crate = rust_extension._discover_crate(workspace, "demo._core")
|
||||
self.assertEqual(crate.source_inputs, (proto.parent.resolve(),))
|
||||
with mock.patch.object(
|
||||
rust_extension,
|
||||
"_command_version",
|
||||
side_effect=lambda command, *args, **kwargs: f"{command} 1.0",
|
||||
):
|
||||
first = rust_extension._build_context(crate)
|
||||
proto.write_text("message Changed {}\n", encoding="utf-8")
|
||||
changed = rust_extension._build_context(crate)
|
||||
|
||||
self.assertNotEqual(first.fingerprint, changed.fingerprint)
|
||||
self.assertEqual(first.target_fingerprint, changed.target_fingerprint)
|
||||
|
||||
def test_auto_builds_once_then_uses_cache(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
@@ -527,30 +558,35 @@ crate-type = ["cdylib"]
|
||||
self.assertNotIn(module_name, sys.modules)
|
||||
|
||||
def test_checked_in_crates_are_discovered_from_wheel_metadata(self):
|
||||
for python_module, package, library, features in (
|
||||
grpc_proto = (rust_extension._RUST_WORKSPACE.parent / "proto").resolve()
|
||||
for python_module, package, library, features, source_inputs in (
|
||||
(
|
||||
"sglang.srt.rust_extensions._server",
|
||||
"sglang-server",
|
||||
"sglang_server",
|
||||
(),
|
||||
(),
|
||||
),
|
||||
(
|
||||
"sglang.srt.rust_extensions._grpc",
|
||||
"sglang-grpc",
|
||||
"sglang_grpc_core",
|
||||
(),
|
||||
(grpc_proto,),
|
||||
),
|
||||
(
|
||||
"sglang.srt.rust_extensions._multimodal",
|
||||
"sglang-mm",
|
||||
"sglang_mm_core",
|
||||
("python", "parallel"),
|
||||
(),
|
||||
),
|
||||
(
|
||||
"sglang.srt.mem_cache.rust_tree_core.mem_cache",
|
||||
"sglang-radix-tree",
|
||||
"mem_cache",
|
||||
("python-extension",),
|
||||
(),
|
||||
),
|
||||
):
|
||||
crate = rust_extension._discover_crate(
|
||||
@@ -559,6 +595,7 @@ crate-type = ["cdylib"]
|
||||
self.assertEqual(crate.package, package)
|
||||
self.assertEqual(crate.library, library)
|
||||
self.assertEqual(crate.features, features)
|
||||
self.assertEqual(crate.source_inputs, source_inputs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -29,6 +29,7 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
InsertParams,
|
||||
InsertResult,
|
||||
MatchPrefixParams,
|
||||
MatchResult,
|
||||
)
|
||||
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
|
||||
from sglang.srt.mem_cache.hicache_storage import (
|
||||
@@ -173,6 +174,51 @@ def test_stale_handle_reads_raise_key_error_without_poisoning_the_core():
|
||||
assert core.is_root(live_root)
|
||||
|
||||
|
||||
def test_stale_match_finalizer_handles_raise_key_error_without_poisoning_the_core():
|
||||
from rust_unified_tree_core_inspector import RustUnifiedTreeCoreInspector
|
||||
|
||||
core = RustUnifiedTreeCoreInspector(
|
||||
CacheInitParams(
|
||||
disable=False,
|
||||
req_to_token_pool=None,
|
||||
token_to_kv_pool_allocator=None,
|
||||
page_size=1,
|
||||
tree_components=(ComponentType.FULL,),
|
||||
)
|
||||
)
|
||||
stale_root = core.root_node_handle()
|
||||
core.reset()
|
||||
live_root = core.root_node_handle()
|
||||
result = MatchResult(
|
||||
device_indices=torch.empty(0, dtype=torch.int64),
|
||||
last_device_node=live_root,
|
||||
last_host_node=live_root,
|
||||
best_match_node=live_root,
|
||||
)
|
||||
params = MatchPrefixParams(key=_key([]))
|
||||
|
||||
for field in ("last_device_node", "last_host_node", "best_match_node"):
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
core.finalize_component_match_result(
|
||||
ComponentType.FULL,
|
||||
result._replace(**{field: stale_root}),
|
||||
params,
|
||||
value_chunks=[],
|
||||
best_value_len=0,
|
||||
)
|
||||
assert exc_info.value.args == (stale_root,), field
|
||||
assert core.is_root(live_root), field
|
||||
|
||||
finalized = core.finalize_component_match_result(
|
||||
ComponentType.FULL,
|
||||
result,
|
||||
params,
|
||||
value_chunks=[],
|
||||
best_value_len=0,
|
||||
)
|
||||
assert finalized.best_match_node == live_root
|
||||
|
||||
|
||||
def test_stale_handle_operations_raise_key_error_without_poisoning_the_core():
|
||||
from sglang.srt.mem_cache.unified_cache.components import CacheTransferPhase
|
||||
|
||||
@@ -180,15 +226,122 @@ def test_stale_handle_operations_raise_key_error_without_poisoning_the_core():
|
||||
stale_root = core.root_node_handle()
|
||||
core.reset()
|
||||
live_root = core.root_node_handle()
|
||||
empty = torch.empty(0, dtype=torch.int64)
|
||||
|
||||
operations = (
|
||||
lambda: core.demote(stale_root),
|
||||
lambda: core.build_hicache_transfers(
|
||||
operations = {
|
||||
"inc_lock_ref": lambda: core.inc_lock_ref(stale_root),
|
||||
"dec_lock_ref": lambda: core.dec_lock_ref(stale_root),
|
||||
"dec_swa_lock_only": lambda: core.dec_swa_lock_only(stale_root, None),
|
||||
"evict_device_leaf": lambda: core.evict_device_leaf(stale_root, False),
|
||||
"drop_subtree_no_host": lambda: core.drop_subtree_no_host(stale_root),
|
||||
"demote": lambda: core.demote(stale_root),
|
||||
"is_full_device_evicted": lambda: core.is_full_device_evicted(stale_root),
|
||||
"collect_full_device_indices/from": lambda: core.collect_full_device_indices(
|
||||
stale_root, live_root
|
||||
),
|
||||
"collect_full_device_indices/until": lambda: core.collect_full_device_indices(
|
||||
live_root, stale_root
|
||||
),
|
||||
"insert_host": lambda: core.insert_host(
|
||||
stale_root, _key([1]), empty, ["0" * 64]
|
||||
),
|
||||
"build_backup_spec": lambda: core.build_backup_spec(stale_root),
|
||||
"build_storage_backup_spec": lambda: core.build_storage_backup_spec(
|
||||
stale_root, False
|
||||
),
|
||||
"build_hicache_transfers": lambda: core.build_hicache_transfers(
|
||||
ComponentType.FULL, stale_root, CacheTransferPhase.BACKUP_STORAGE
|
||||
),
|
||||
lambda: core.build_load_back_spec(stale_root),
|
||||
lambda: core.get_hash_values(stale_root),
|
||||
lambda: core.dfs_weight_order([stale_root]),
|
||||
"commit_backup": lambda: core.commit_backup(stale_root, empty, {}),
|
||||
"commit_hicache_transfers": lambda: core.commit_hicache_transfers(
|
||||
stale_root,
|
||||
CacheTransferPhase.BACKUP_HOST,
|
||||
{},
|
||||
cache_actions=[],
|
||||
),
|
||||
"commit_load_back": lambda: core.commit_load_back(
|
||||
stale_root, empty, PoolTransfer(name=PoolName.KV), {}
|
||||
),
|
||||
"build_load_back_spec": lambda: core.build_load_back_spec(stale_root),
|
||||
"evict_excess_path_states": lambda: core.evict_excess_path_states(
|
||||
stale_root, {}, {}
|
||||
),
|
||||
"inc_host_lock_ref": lambda: core.inc_host_lock_ref(stale_root),
|
||||
"dec_host_lock_ref": lambda: core.dec_host_lock_ref(stale_root),
|
||||
"mark_write_through_pending": lambda: core.mark_write_through_pending(
|
||||
[stale_root], stale_root
|
||||
),
|
||||
"finish_write_through": lambda: core.finish_write_through(
|
||||
[stale_root], stale_root
|
||||
),
|
||||
"finish_load_back": lambda: core.finish_load_back(stale_root),
|
||||
"get_component_device_value": lambda: core.get_component_device_value(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"component_has_host_value_only": lambda: core.component_has_host_value_only(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"get_hash_values": lambda: core.get_hash_values(stale_root),
|
||||
"dfs_weight_order": lambda: core.dfs_weight_order([stale_root]),
|
||||
}
|
||||
for name, operation in operations.items():
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
operation()
|
||||
assert exc_info.value.args == (stale_root,), name
|
||||
assert core.is_root(live_root), name
|
||||
|
||||
|
||||
def test_stale_handles_nested_in_transfer_results_do_not_poison_the_core():
|
||||
from sglang.srt.mem_cache.unified_cache.components import CacheTransferPhase
|
||||
|
||||
core = _tree_core()
|
||||
stale_root = core.root_node_handle()
|
||||
core.reset()
|
||||
live_root = core.root_node_handle()
|
||||
stale_transfer = PoolTransfer(name=PoolName.KV, nodes_to_load=[stale_root])
|
||||
|
||||
operations = (
|
||||
lambda: core.commit_hicache_transfers(
|
||||
live_root,
|
||||
CacheTransferPhase.LOAD_BACK,
|
||||
{ComponentType.FULL: [stale_transfer]},
|
||||
cache_actions=[],
|
||||
),
|
||||
lambda: core.commit_hicache_transfers(
|
||||
live_root,
|
||||
CacheTransferPhase.PREFETCH,
|
||||
{},
|
||||
cache_actions=[],
|
||||
insert_result=InsertResult(prefix_len=0, inserted_host_node=stale_root),
|
||||
),
|
||||
lambda: core.commit_load_back(
|
||||
live_root,
|
||||
torch.empty(0, dtype=torch.int64),
|
||||
stale_transfer,
|
||||
{},
|
||||
),
|
||||
)
|
||||
for operation in operations:
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
operation()
|
||||
assert exc_info.value.args == (stale_root,)
|
||||
assert core.is_root(live_root)
|
||||
|
||||
|
||||
def test_stale_handle_component_access_does_not_poison_the_core():
|
||||
core = _tree_core(
|
||||
tree_components=(ComponentType.FULL, ComponentType.SWA),
|
||||
sliding_window_size=8,
|
||||
)
|
||||
stale_root = core.root_node_handle()
|
||||
core.reset()
|
||||
live_root = core.root_node_handle()
|
||||
|
||||
operations = (
|
||||
lambda: core.set_component_device_value(
|
||||
stale_root, ComponentType.SWA, torch.empty(0, dtype=torch.int64)
|
||||
),
|
||||
lambda: core.get_component_device_value(stale_root, ComponentType.SWA),
|
||||
)
|
||||
for operation in operations:
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
@@ -1261,6 +1414,10 @@ def test_buffer_backup_snapshot_round_trips_and_detects_a_split():
|
||||
)
|
||||
assert core.validate_buffer_backup(leaf, len(snapshot.key)) is None
|
||||
|
||||
core.reset()
|
||||
assert core.snapshot_buffer_backup(leaf, pass_prefix_keys=True) is None
|
||||
assert core.validate_buffer_backup(leaf, len(snapshot.key)) is None
|
||||
|
||||
|
||||
def test_buffer_backup_snapshot_preserves_bigram_keys():
|
||||
core = _tree_core(is_eagle=True)
|
||||
@@ -1996,5 +2153,122 @@ def test_bigram_insert_value_shorter_than_the_bigram_count_raises():
|
||||
)
|
||||
|
||||
|
||||
def test_stale_inspection_handles_raise_key_error_or_report_absence():
|
||||
from rust_unified_tree_core_inspector import RustUnifiedTreeCoreInspector
|
||||
|
||||
from sglang.srt.mem_cache.unified_cache.components import EvictLayer
|
||||
|
||||
core = RustUnifiedTreeCoreInspector(
|
||||
CacheInitParams(
|
||||
disable=False,
|
||||
req_to_token_pool=None,
|
||||
token_to_kv_pool_allocator=None,
|
||||
page_size=1,
|
||||
tree_components=(ComponentType.FULL,),
|
||||
)
|
||||
)
|
||||
stale_root = core.root_node_handle()
|
||||
core.reset()
|
||||
live_root = core.root_node_handle()
|
||||
|
||||
operations = {
|
||||
"get_parent_node_id": lambda: core.get_parent_node_id(stale_root),
|
||||
"get_child_node_ids": lambda: core.get_child_node_ids(stale_root),
|
||||
"get_node_key_length": lambda: core.get_node_key_length(stale_root),
|
||||
"get_node_token_ids": lambda: core.get_node_token_ids(stale_root),
|
||||
"is_node_key_bigram": lambda: core.is_node_key_bigram(stale_root),
|
||||
"get_component_host_value": lambda: core.get_component_host_value(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"get_component_device_lock_ref": lambda: core.get_component_device_lock_ref(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"get_node_hit_count": lambda: core.get_node_hit_count(stale_root),
|
||||
"get_write_through_pending_id": lambda: core.get_write_through_pending_id(
|
||||
stale_root
|
||||
),
|
||||
"is_node_in_device_lru": lambda: core.is_node_in_device_lru(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"is_node_in_host_lru": lambda: core.is_node_in_host_lru(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"is_device_leaf": lambda: core.is_device_leaf(stale_root),
|
||||
"set_node_hash_values": lambda: core.set_node_hash_values(stale_root, None),
|
||||
"set_component_device_value_raw": lambda: core.set_component_device_value_raw(
|
||||
stale_root, ComponentType.FULL, None
|
||||
),
|
||||
"set_component_host_value_raw": lambda: core.set_component_host_value_raw(
|
||||
stale_root, ComponentType.FULL, None
|
||||
),
|
||||
"set_component_device_lock_ref": lambda: core.set_component_device_lock_ref(
|
||||
stale_root, ComponentType.FULL, 0
|
||||
),
|
||||
"remove_node_from_device_lru": lambda: core.remove_node_from_device_lru(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"insert_node_into_host_lru": lambda: core.insert_node_into_host_lru(
|
||||
stale_root, ComponentType.FULL
|
||||
),
|
||||
"update_duplicate_tracking": lambda: core.update_duplicate_tracking(stale_root),
|
||||
"evict_component": lambda: core.evict_component(
|
||||
stale_root, ComponentType.FULL, EvictLayer.DEVICE
|
||||
),
|
||||
"validate_cascade_evict": lambda: core.validate_cascade_evict(
|
||||
stale_root, ComponentType.FULL, EvictLayer.DEVICE
|
||||
),
|
||||
"cleanup_tombstone_ancestors": lambda: core.cleanup_tombstone_ancestors(
|
||||
stale_root
|
||||
),
|
||||
"build_backup_node_ids": lambda: core.build_backup_node_ids(stale_root),
|
||||
}
|
||||
for name, operation in operations.items():
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
operation()
|
||||
assert exc_info.value.args == (stale_root,), name
|
||||
assert core.is_root(live_root), name
|
||||
|
||||
disabled_component_operations = {
|
||||
"get_component_host_value": lambda: core.get_component_host_value(
|
||||
stale_root, ComponentType.SWA
|
||||
),
|
||||
"get_component_device_lock_ref": lambda: core.get_component_device_lock_ref(
|
||||
stale_root, ComponentType.SWA
|
||||
),
|
||||
"set_component_device_value_raw": lambda: core.set_component_device_value_raw(
|
||||
stale_root, ComponentType.SWA, None
|
||||
),
|
||||
"set_component_host_value_raw": lambda: core.set_component_host_value_raw(
|
||||
stale_root, ComponentType.SWA, None
|
||||
),
|
||||
"set_component_device_lock_ref": lambda: core.set_component_device_lock_ref(
|
||||
stale_root, ComponentType.SWA, 0
|
||||
),
|
||||
"remove_node_from_device_lru": lambda: core.remove_node_from_device_lru(
|
||||
stale_root, ComponentType.SWA
|
||||
),
|
||||
"insert_node_into_host_lru": lambda: core.insert_node_into_host_lru(
|
||||
stale_root, ComponentType.SWA
|
||||
),
|
||||
"evict_component": lambda: core.evict_component(
|
||||
stale_root, ComponentType.SWA, EvictLayer.DEVICE
|
||||
),
|
||||
"validate_cascade_evict": lambda: core.validate_cascade_evict(
|
||||
stale_root, ComponentType.SWA, EvictLayer.DEVICE
|
||||
),
|
||||
}
|
||||
for name, operation in disabled_component_operations.items():
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
operation()
|
||||
assert exc_info.value.args == (stale_root,), name
|
||||
assert core.is_root(live_root), name
|
||||
|
||||
assert not core.contains_node(stale_root)
|
||||
assert not core.is_device_evictable_leaf(stale_root)
|
||||
assert not core.is_host_evictable_leaf(stale_root)
|
||||
assert not core.is_node_in_device_lru(stale_root, ComponentType.SWA)
|
||||
assert not core.is_node_in_host_lru(stale_root, ComponentType.SWA)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
|
||||
@@ -8065,6 +8065,61 @@ class _InsertWalkSuite(CustomTestCase):
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(torch.cuda.is_available(), "cache fixtures need CUDA")
|
||||
class TestUnifiedTreeCoreSWAPrefetchBackends(_InsertWalkSuite):
|
||||
cfg = CacheConfig(
|
||||
components=(ComponentType.FULL, ComponentType.SWA), sliding_window_size=4
|
||||
)
|
||||
|
||||
def test_mid_tree_shortened_swa_prefetch_is_released(self):
|
||||
cache, allocator, req_to_token_pool = build_fixture(self.cfg)
|
||||
prefix = [1, 2]
|
||||
self._insert(cache, allocator, req_to_token_pool, prefix)
|
||||
anchor = cache.match_prefix(
|
||||
MatchPrefixParams(key=RadixKey(array("q", prefix)))
|
||||
).last_device_node
|
||||
|
||||
cache.tree_core.is_write_back = True
|
||||
suffix = [3, 4]
|
||||
insert_result = cache.tree_core.insert_host(
|
||||
anchor,
|
||||
RadixKey(array("q", suffix)),
|
||||
torch.tensor([100, 101], dtype=torch.int64),
|
||||
["h3", "h4"],
|
||||
)
|
||||
self.assertIsNotNone(insert_result.inserted_host_node)
|
||||
|
||||
swa_host_indices = torch.tensor([30, 31], dtype=torch.int64)
|
||||
actions = []
|
||||
cache.tree_core.commit_hicache_transfers(
|
||||
anchor,
|
||||
CacheTransferPhase.PREFETCH,
|
||||
{
|
||||
ComponentType.SWA: [
|
||||
PoolTransfer(
|
||||
name=PoolName.SWA,
|
||||
host_indices=swa_host_indices,
|
||||
)
|
||||
]
|
||||
},
|
||||
cache_actions=actions,
|
||||
insert_result=insert_result,
|
||||
pool_storage_result=PoolTransferResult(
|
||||
kv_hit_pages=len(suffix),
|
||||
extra_pool_hit_pages={PoolName.SWA: len(suffix)},
|
||||
),
|
||||
)
|
||||
|
||||
self.assertIsNone(
|
||||
_host_value(cache, insert_result.inserted_host_node, ComponentType.SWA)
|
||||
)
|
||||
self.assertEqual(len(actions), 1)
|
||||
self.assertIsInstance(actions[0], FreeComponentHostSlot)
|
||||
self.assertEqual(actions[0].component_type, ComponentType.SWA)
|
||||
self.assertEqual(len(actions[0].host_indices), 1)
|
||||
self.assertTrue(torch.equal(actions[0].host_indices[0], swa_host_indices))
|
||||
|
||||
|
||||
@unittest.skipUnless(torch.cuda.is_available(), "cache fixtures need CUDA")
|
||||
class TestResumableInsertWalk(_InsertWalkSuite):
|
||||
cfg = CacheConfig()
|
||||
|
||||
Reference in New Issue
Block a user