Skip to content

[feat] simple storage support ssd offload - #162

Merged
0oshowero0 merged 3 commits into
Ascend:mainfrom
xupinjie:pinjie/simple_storage_offload_ssd
Sep 24, 2026
Merged

0oshowero0 merged 3 commits into
Ascend:mainfrom
xupinjie:pinjie/simple_storage_offload_ssd

Conversation

@xupinjie

@xupinjie xupinjie commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Large training samples can make the long-lived host-memory footprint of SimpleStorage a capacity bottleneck as training scale and sequence length increase.

Design

This PR adds an optional hybrid storage layer inside SimpleStorageUnit without changing the public API, controller, or client behavior. Each (field, global index) sample is routed independently according to its actual stored payload size: samples at or above the internal 1 MiB threshold are written to node-local SSD, while smaller samples remain in host memory. The hybrid store preserves existing PUT, GET, CLEAR, overwrite, capacity, and checkpoint semantics; it reuses decoded contiguous buffers when possible and falls back to pickle for composite values. SSD files are temporary, isolated by run and storage unit, and removed during clear or graceful shutdown. The feature is disabled by default and configured through ssd_offload.enabled and ssd_offload.path.

E2E Performance

The following results are averages over four iterations with a batch size of 512 and eight fields. Throughput is reported in Gbit/s.

Test environment: 2 × Intel Xeon Platinum 8480C CPUs (224 cores), 10 × NVIDIA/Mellanox ConnectX-7 NDR 400 Gb/s InfiniBand adapters plus an Intel E810-C 100 GbE adapter, and 8 × KIOXIA CM6-R 3.84 TB NVMe SSDs (KCM6DRUL3T84). 2 Node.

Per-field sample size Total payload Memory PUT SSD PUT Memory GET SSD GET Memory E2E SSD E2E E2E change
1 MiB 4 GiB 55.35 51.33 29.58 28.42 38.34 36.44 -5.0%
2 MiB 8 GiB 57.17 53.53 25.37 24.65 35.04 33.73 -3.7%
4 MiB 16 GiB 58.18 50.80 27.60 26.06 37.39 34.41 -8.0%

@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-01T04:01:12.713288Z ed97682 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed9768257d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

done
done

python "${SCRIPT_DIR}/draw_figure.py"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use an SSD-compatible result plotter

When this documented SSD benchmark runs with a clean results/ directory, every generated filename ends in sample1mib, sample2mib, or sample4mib, but draw_figure.py accepts only small, medium, and large; it skips every new CSV and then fails at pd.concat([]). If older standard benchmark CSVs exist, it instead produces a chart from those stale, unrelated results. Thus the expensive benchmark either exits unsuccessfully or emits a misleading comparison rather than the advertised SSD chart; extend the plotter for these names/scenarios or invoke a dedicated SSD plotter.

AGENTS.md reference: AGENTS.md:L17-L18

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I delete the draw command.

@0oshowero0
0oshowero0 requested a balanced review from Copilot September 1, 2026 07:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@@ -0,0 +1,85 @@
#!/bin/bash

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we integrate the SSD test in existing test shell scripts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation uses a 1 MB threshold per sample, but the data sizes in the existing benchmark scripts are too small to trigger the offloading mechanism.

Comment thread tests/test_simple_storage_unit.py Outdated
storage.close()


def test_hybrid_storage_round_trips_raw_tensor_samples(tmp_path):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may not need some of the tests here. Consider simplify the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted some test.

Comment thread transfer_queue/config.yaml Outdated
enabled: false
# Directory on a local NVMe SSD where offload files are written.
# Each storage unit creates its own sub-directory; files are removed on shutdown.
path: /tmp/tq_ssd_offload

@0oshowero0 0oshowero0 Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/tmp usually is not a SSD path, but a tmpfs RAM space. This may explain the high performance in PR description. If this is the case, we have to rerun it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the default value to null, so users must explicitly provide a path. I also removed the code that handled the previous default value and added validation to ensure that at least the parent directory of the user-provided path exists.

@xupinjie
xupinjie force-pushed the pinjie/simple_storage_offload_ssd branch from ed97682 to 6b636de Compare September 1, 2026 15:33
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@xupinjie
xupinjie force-pushed the pinjie/simple_storage_offload_ssd branch from 6b636de to 988f095 Compare September 2, 2026 02:10
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@xupinjie
xupinjie force-pushed the pinjie/simple_storage_offload_ssd branch from 988f095 to b1d5412 Compare September 6, 2026 11:08
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

Comment on lines +978 to +980
storage_unit_size: int | None = None,
ssd_config=None,
ssd_run_id: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to pass the simple storage related config in a single config param, e.g. https://github.com/Ascend/TransferQueue/blob/main/transfer_queue/storage/clients/mooncake_client.py#L56

Comment thread transfer_queue/config.yaml Outdated
ssd_offload:
# Master switch. Set to true to enable SSD offload.
enabled: false
# Required when enabled. Its parent must exist on a local NVMe SSD;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should allow the case when parent dir not exist.

Comment on lines +368 to +372
fd = os.open(temp_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
self._write_many(fd, [sample.payload])
finally:
os.close(fd)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can try to find some existing library that can help us to enable io_uring

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. For this PR, we intentionally use a small thread pool with regular POSIX file I/O to keep the initial implementation simple and avoid introducing additional dependencies. I plan to evaluate existing io_uring libraries in a follow-up PR, together with dedicated performance benchmarks.

@huniu20

huniu20 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Great work! I've already verified the effectiveness and stability of offloading in long runs locally. That said, there's one point worth raising:

Symptom: After enabling SSD offload, storage unit RSS actually increases (+75% on the data plane in our 4-node test), even though payloads are correctly written to disk and everything functions normally.

Diagnosis: Call malloc_trim(0) after CLEAR and check whether RSS drops. If it does, the memory is unreturned free heap space (high-water mark), not data still being held. Our test confirms it drops, so this isn't a reference leak in the code.

Mechanism: The contiguous payload buffers generated by _build_encoded_samples for disk writes trigger glibc's dynamic mmap threshold adjustment — after the first large mmap'd block is freed, the threshold is raised to that block's size, and subsequent same-sized allocations are served from the heap and no longer returned to the OS. RSS stays at the high-water mark as a result.

Fix: Set MALLOC_MMAP_THRESHOLD_ on the storage actor below the typical payload size. Scope it narrowly — setting it globally imposes unnecessary mmap/munmap overhead on training/inference hot paths that benefit from heap reuse.

Suggestion: Consider adding an RSS measurement to run_ssd_offload_perf_test.sh. This PR's motivation is reducing host memory footprint, but the current self-test only covers throughput.

@xupinjie
xupinjie force-pushed the pinjie/simple_storage_offload_ssd branch from b1d5412 to 0aec5e7 Compare September 17, 2026 10:13
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@xupinjie
xupinjie marked this pull request as draft September 17, 2026 10:14
Comment thread transfer_queue/config.yaml Outdated
Comment on lines +45 to +48
# Required when enabled. TransferQueue creates this directory and any
# missing parents on every storage node, stores data under its
# transfer_queue_ssd_offload/ child, and leaves this directory intact on close.
# Example: /path/to/local/ssd

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify the comments. Best fit in one line

Comment thread transfer_queue/config.yaml Outdated
Comment on lines +38 to +41
# SSD offload reduces the long-lived host-memory footprint by keeping large
# fields on local SSD. Small fields remain in memory.
# Each offloaded (field, global_index) sample owns one temporary file so
# CLEAR can reclaim its disk space immediately.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we provide a dedicate doc to describe the implementation details

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add a new file docs/ssd_offload.md.


TQ_STORAGE_POLLER_TIMEOUT = int(os.environ.get("TQ_STORAGE_POLLER_TIMEOUT", 5)) # in seconds
TQ_NUM_THREADS = int(os.environ.get("TQ_NUM_THREADS", 8))
DEFAULT_SSD_OFFLOAD_THRESHOLD_BYTES = 1024 * 1024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value is inconsistent with config.yaml

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python:1024 * 1024 = 1048576
YAML:threshold_bytes: 1048576
They are both 1 MiB (1,048,576 bytes).

@xupinjie
xupinjie force-pushed the pinjie/simple_storage_offload_ssd branch from 1f436c2 to 5f2c7eb Compare September 20, 2026 15:01
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@0oshowero0

Copy link
Copy Markdown
Collaborator

scripts/put_benchmark.py is broken: SimpleStorageUnit.init now only accepts config. Please help to fix this benchmark

@staticmethod
def _sample_from_value(value: Any) -> SSDEncodedSample | None:
if isinstance(value, torch.Tensor):
if value.is_nested or value.is_sparse:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add support for sparse tensors now? Also, data larger than threshold_bytes but not eligible for SSD offload currently falls back to DRAM silently — could we add some metrics for this so that we can observe it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I've added tq_storage_ssd_fallback_values_total, a per-storage-unit counter that tracks values that cannot be encoded for SSD offload and therefore remain in DRAM. This makes silent fallbacks observable through the metrics endpoint, and I've documented its semantics as well. For sparse tensors and broader data type support, I'd prefer to address those in a follow-up PR so that we can define and test the support matrix separately.

Signed-off-by: pinjie <pinjiex@nvidia.com>
@xupinjie
xupinjie force-pushed the pinjie/simple_storage_offload_ssd branch from 5f2c7eb to 874934b Compare September 23, 2026 03:30
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@xupinjie

xupinjie commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Impact of glibc_mmap_threshold_bytes on SSD Offload Throughput and RSS Memory Usage

glibc_mmap_threshold_bytes RSS before kv_clear RSS after kv_clear Matched in-memory run: RSS before kv_clear kv_batch_put throughput kv_batch_get throughput
unset (null) 11.858 GiB 11.787 GiB 13.205 GiB 44.14 Gbit/s 36.69 Gbit/s
128 KiB 4.782 GiB 4.045 GiB 13.570 GiB 44.53 Gbit/s 38.24 Gbit/s
1 MiB 4.747 GiB 4.081 GiB 13.376 GiB 44.90 Gbit/s 37.99 Gbit/s
4 MiB 4.519 GiB 3.799 GiB 12.902 GiB 44.72 Gbit/s 38.12 Gbit/s

The benchmark ran on two nodes with Arm Neoverse V2 CPUs and NVIDIA/Mellanox ConnectX-6 Lx networking. Each iteration processed 5 GiB of data, with 4 GiB (80%) offloaded to local SSD. Each configuration ran 10 consecutive PUT–GET–CLEAR iterations, and the table reports results from the final iteration.

cc @huniu20 @0oshowero0

Signed-off-by: pinjie <pinjiex@nvidia.com>
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

Signed-off-by: pinjie <pinjiex@nvidia.com>
@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

xupinjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@0oshowero0
0oshowero0 merged commit 4cc0d7f into Ascend:main Sep 24, 2026
8 checks passed
Ginray added a commit to Ginray/TransferQueue that referenced this pull request Oct 9, 2026
Sync 11 main PRs (Ascend#162, Ascend#167, Ascend#168, Ascend#170-Ascend#172, Ascend#174, Ascend#175, Ascend#177-Ascend#179)
into the payload-transfer branch. Main's PR history is preserved; the
payload transfer abstraction stays on top.

Conflict resolutions:
- SimpleStorageUnit: keep main's config-dict constructor, hybrid
  memory/SSD storage, proxy identity filter, arrival counters, accept
  probe, and checkpoint delegation; the worker dispatches PUT/GET
  families to the configured PayloadTransfer as before.
- AsyncSimpleStorageManager: keep main's socket pools, retry with
  diagnosis, and heavy-request logging; per-unit put/get delegate to
  the configured PayloadTransfer.
- Bootstrap: keep main's config-dict actor construction plus payload
  transfer validation and NIXL endpoint collection.

Semantic adaptations (not textual conflicts):
- Add PayloadTransferTimeout (base.py) carrying wire_bytes; both
  backends raise it on an unanswered control message, and the manager
  translates it to StorageUnitTimeout so Ascend#171 retry/diagnosis covers
  the ZMQ and NIXL-UCX paths uniformly.
- PayloadTransfer.put returns payload bytes moved, so Ascend#171's manager
  side heavy-put logging keeps true wire sizes per backend.

Verified: python -m compileall -q transfer_queue tutorial tests clean;
ruff check clean; pytest 622 passed, 8 skipped, 8 errors (the 8 errors
are the pre-existing optional-Yuanrong-backend baseline).
0oshowero0 pushed a commit that referenced this pull request Oct 9, 2026
## Summary

Adds two operations on existing KV keys:

- **`kv_update` / `kv_batch_update`** merge new values into fields that
are already stored, using `merge_fn(old, new)`. The merge runs on the
SimpleStorage unit that holds each sample, so the stored payload never
makes a round trip through the client.
- **`kv_clear(keys, partition_id, fields=...)`** clears only the given
fields and keeps the keys, for example to release `pixel_values` after
rollout while training still reads the rest of the sample. It works on
every storage backend. Without `fields`, `kv_clear` behaves as before.

To make multi-field updates all-or-nothing on a unit, SSD-offloaded
`put_data` is now two-phase, which also affects plain multi-field `put`
(see "SSD offload").

## Usage

### `kv_update`: append the response to the prompt

```python
import torch
import transfer_queue as tq

tq.kv_put(key="s0", partition_id="train", fields={
    "input_ids": torch.tensor([10, 11, 12]),
    "attention_mask": torch.tensor([1, 1, 1]),
})

tq.kv_update(
    key="s0",
    partition_id="train",
    fields={"input_ids": torch.tensor([20, 21]), "attention_mask": torch.tensor([1, 1])},
    merge_fn=lambda old, new: torch.cat([old, new]),
)
# input_ids      -> tensor([10, 11, 12, 20, 21])
# attention_mask -> tensor([1, 1, 1, 1, 1])
```

For a batch, `kv_batch_update(keys, partition_id, fields: TensorDict,
merge_fn)` takes a TensorDict whose batch size matches `keys` (nested
tensors are allowed here). Both have `async_` variants.

### `kv_clear(fields=...)`: release pixel values after rollout

In a multimodal RL step, the rollout engine needs the raw
`pixel_values`, while training only needs the processed tensors. Once
rollout is done, the pixel values only take up storage:

```python
tq.kv_batch_put(keys=keys, partition_id="train", fields=TensorDict({
    "input_ids": input_ids,
    "pixel_values": pixel_values,  # large; only rollout reads it
    "mm_tensors": mm_tensors,      # what training reads
}, batch_size=len(keys)))

# ... rollout reads pixel_values and writes the responses back ...

tq.kv_clear(keys=keys, partition_id="train", fields="pixel_values")

# Training reads input_ids / responses / mm_tensors exactly as before.
```

After a field-level clear:

- The storage backend no longer holds the values. With SSD offload on,
their SSD files are deleted.
- The field reads as never written for those keys: it is not ready, and
the controller no longer describes its old shape or dtype for them.
- The keys, their other fields, tags and consumption status are
unchanged, so samples are not consumed again.
- A later `kv_put` to the field writes it normally and marks it ready
again.
- Missing keys and fields are ignored, matching whole-key `kv_clear`.
Clearing every field still keeps the key in `kv_list`; removing the key
itself is still `kv_clear` without `fields`.

## Design

### `kv_update` (data plane, SimpleStorage only)

`interface.async_kv_batch_update` → `client.async_update` →
`AsyncSimpleStorageManager.update_data` → new ZMQ request `UPDATE_DATA`
→ `SimpleStorageUnit._handle_update` → `StorageUnitData.apply_update`.

- **Preconditions.** Every key must exist and every field must already
be produced for every key; otherwise `ValueError`. Nothing is created.
- **Compute, validate, then write once.** Each unit computes every merge
result and checks that each field keeps its tensor/non-tensor type and
tensor dtype, then commits with a single `put_data`. A raising
`merge_fn` or an incompatible result leaves that unit unchanged.
- **Schema.** Each unit reports the shapes it stored; the manager
assembles one batch-level `field_schema` in `global_indexes` order (the
controller reads `per_sample_shapes` by position) and notifies the
controller. The client merges it into its `BatchMeta` with the new
`BatchMeta.apply_field_schema` (`add_fields` now uses the same helper).
- **Controller ACK is checked.** `notify_data_update` now returns the
ACK's `success`. On the update path, a rejected or missing ACK raises
`RuntimeError` stating that storage may already have committed and the
merge must not be retried blindly.
- **No replays.** Updates use `max_attempts=1`, because a replay after a
lost reply would fold the same value in twice. A timeout therefore has
an unknown outcome: the unit may commit after the caller stops waiting.
This is documented on the API.
- **Atomicity is per storage-unit request.** A batch spanning several
units is not a distributed transaction.
- **Other backends.** The base `StorageManager.update_data` raises
`NotImplementedError("kv_update is not supported by <ManagerClass>; it
requires the SimpleStorage backend")`. KV backends store each
sample-field under its own key and have no hook to run `merge_fn` where
the value lives.

### Field-level `kv_clear` (control plane first, then storage)

`interface.async_kv_clear(fields=...)` → `client.async_kv_clear_fields`
→ new controller request `KV_CLEAR_FIELDS` →
`storage_manager.clear_data(metadata, fields=...)`.

- **Controller first.** `TransferQueueController.kv_clear_fields`
resolves the keys and captures the metadata of the affected cells,
including custom backend metadata that KV backends need for deletion.
Then `DataPartitionStatus.clear_fields` zeroes those production-status
cells, calls the existing `FieldMeta.remove_samples` (which already
falls back from nested to regular), and drops the per-field backend
metadata. It does not touch key mappings, tags, consumption status or
indexes, so there is no index-reuse race and no `MARK_CLEARING` step is
needed.
- **Then storage.** `clear_data` gains an optional `fields` filter. KV
backends (Mooncake, Yuanrong, Ray) narrow the metadata with
`select_fields`. SimpleStorage forwards `fields` in `CLEAR_DATA`, and
units drop only those fields, unlinking their SSD files; the sample
stays counted against unit capacity.
- The controller resolves fields per cell rather than relying on
`kv_retrieve_meta`, which only reports fields that every requested key
has, so a field present on only some keys is still cleared on those
keys.

### SSD offload: all-or-nothing multi-field put (#162)

`HybridStorageUnitData.put_data` used to write, commit and clean up one
field at a time, so a failure on a later field (for example, a full
disk) left earlier fields replaced. It is now two-phase: write every
field's SSD files first (on failure, unlink only the files this call
wrote and re-raise), then swap all references and unlink superseded
files. The base `StorageUnitData.put_data` checks every field's length
before writing any.

**This also affects plain multi-field `put` with SSD offload on.** The
trade-off is a higher temporary peak: the old files plus all replacement
files exist at once. `docs/ssd_offload.md` documents it.

### Shared backing memory

SimpleStorage slices dense batches without copying, so samples written
by one batched put share an allocation. Replacing or clearing some of
them does not return memory until every sibling is replaced or cleared.
`docs/ssd_offload.md` documents this alongside the existing
`data_parser` note.

## Docs and tutorial

- **README:** `(async_)kv_update` / `(async_)kv_batch_update` added,
`kv_clear` notes the `fields` option, and the
`StorageManager.clear_data` signature is updated.
- **`tutorial/02_kv_interface.py`:** a new step that concatenates a
field with `kv_update` and then clears it with `kv_clear(fields=...)`.
- **`docs/ssd_offload.md`:** shared backing memory and SSD headroom for
atomic multi-field writes.

## Testing

- `python -m compileall -q transfer_queue tutorial tests` and pre-commit
(ruff, ruff-format, mypy) are clean.
- `tests/e2e/test_kv_interface_e2e.py` on SimpleStorage with 2 units,
sync and async: **64 passed, 2 skipped** (the skips are KV-backend-only
rejection tests).
- Full local suite, `python -m pytest -q`: **699 passed, 12 skipped, 8
errors**. The errors are all in
`tests/test_yuanrong_storage_client_e2e.py` at fixture setup, because
the Yuanrong `datasystem` package is not installed locally.
- Not run: the Mooncake and Yuanrong e2e backends.

New and updated tests:

- `tests/test_kv_update.py`: merge, atomicity on merge error or
incompatible result, unproduced field rejection, decoded SSD-offloaded
old values, atomicity when a later field fails to reach SSD, cross-unit
schema assembly, KV-backend rejection naming the class.
- `tests/test_storage_request_retry.py`: the controller ACK status is
returned; a rejected ACK after a storage commit raises; an update
timeout is neither retried nor published.
- `tests/test_controller_data_partitions.py`: field-level clear keeps
keys, tags, consumption status and other fields; the cleared field's
schema for the remaining samples is unchanged; a later nested write to
the same field succeeds; `FieldMeta` is dropped once no sample holds the
field.
- `tests/test_simple_storage_unit.py`: field-level clear in memory and
on SSD (only that field's file is unlinked); all-or-nothing multi-field
SSD put.
- `tests/test_kv_storage_manager.py`: `clear_data(fields=...)` deletes
only those keys with their backend metadata.
- `tests/e2e/test_kv_interface_e2e.py`: `TestKVUpdateE2E` (update then
clear a field, then nested writes; batch update across units; rejected
arguments) and
`TestKVClearE2E::test_kv_clear_fields_keeps_keys_and_other_fields`.

## Out of scope

- `kv_batch_get(select_fields=...)` on a field that is not ready returns
an empty result rather than raising. This is existing behavior.

---------

Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants