Repository navigation
[feat] Support data_parser in kv_batch_get for SimpleStorage - #191
Conversation
kv_batch_get / async_kv_batch_get take parser(value, parser_arg) plus per-key parser_args. SimpleStorage units apply the parser to every selected (key, field) before serializing the reply, so only the parsed part crosses the network; select_list_items covers picking items out of a list field. - Parser errors come back as RuntimeError naming field, global index and arg. - A parser get requires every key to exist, so args stay aligned with keys. - Replies carry "parsed"; the manager rejects replies from older units. - KV backends raise NotImplementedError instead of fetching whole values. (cherry picked from commit 9c240f3f61c8d58ee6fed056dc5312060ce7f7cf) Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
(cherry picked from commit 93e9bdf44784e82b8bb32fde2d1f1e045e60cca0) Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
… routing - kv_batch_get docs: the parser runs on every selected field, synchronously on the storage unit worker; pair it with select_fields and keep it light. - apply_get_parser docs: kv_batch_get callers see parser errors as RuntimeError. - Manager tests: parser_args are sliced per storage unit by batch position, and a reply without the parsed flag is rejected. (cherry picked from commit 47de4213780b4a07fab6fa4561859040badb78f1) Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
Let kv_batch_get return a contiguous view of a stored tensor so only the sliced bytes are serialized on the storage unit, matching select_list_items for list fields. Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
Document contiguous encode vs SSD I/O honestly; coerce slice_tensor length args only; fail early when a parser omits select_fields on multi-field keys; clarify list vs tensor index asymmetry with regressions. Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0058571ee3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| target_num_threads=TQ_NUM_THREADS, info=f"[{self.client_id}] async_get_data" | ||
| ): | ||
| results = await self.storage_manager.get_data(metadata) | ||
| results = await self.storage_manager.get_data(metadata, parser=parser, parser_args=parser_args) |
There was a problem hiding this comment.
Preserve existing custom StorageManager gets
For a custom StorageManager implementing the documented get_data(self, metadata) extension contract, an ordinary get with no parser now still passes both new keywords and raises TypeError: unexpected keyword argument 'parser'. Such subclasses remain concrete and registerable because ABC does not check override signatures, and README.md still advertises the old signature, so only pass these keywords when parsing is requested or provide a compatibility path.
AGENTS.md reference: AGENTS.md:L17-L18
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Option A: done in f15dd10 (woa cherry-pick 1b61e9b).
async_get_data only passes parser / parser_args when a parser is set, so custom managers with get_data(self, metadata) keep working for plain gets. README notes the optional kwargs; test_async_get_data_omits_parser_kwargs_for_old_managers covers the regression.
There was a problem hiding this comment.
Still handled after 82ea7f9. async_get_data passes select_items only when it is set, so managers written as get_data(self, metadata) keep serving plain gets (test_async_get_data_passes_select_items_only_when_set).
There was a problem hiding this comment.
Update: still handled after 61b4f9e, which replaced select_items with data_parser / parser_args. async_get_data passes them to StorageManager.get_data only when data_parser is set, so managers written as get_data(self, metadata) keep serving plain gets. The test is now test_async_get_data_passes_data_parser_only_when_set.
| result_data, parser, data_parts.body.get("parser_args"), global_indexes | ||
| ) | ||
|
|
||
| response_body: dict[str, Any] = {"data": result_data} |
There was a problem hiding this comment.
Catch parser response serialization failures
When a custom parser returns an object that neither msgpack nor pickle can serialize (for example, a generator or the coroutine produced by an accidentally async parser), this handler builds a success response containing it. _process_one_worker_request calls response_msg.serialize() after its exception guard, so the pickle fallback raises out of _worker_routine and permanently terminates the storage unit's sole worker thread instead of returning a parser error; response serialization failures need to be contained and converted to a basic error reply.
AGENTS.md reference: AGENTS.md:L17-L18
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Option A: done in f15dd10 (woa cherry-pick 1b61e9b).
Response serialize() is now wrapped in try/except in the worker path: failures become GET_ERROR / PUT_GET_ERROR instead of killing the sole worker. Covered by test_unserializable_get_response_answered_and_worker_survives.
There was a problem hiding this comment.
Superseded by 82ea7f9. The storage unit no longer runs user code: select_items only slices or indexes values that were already encodable. The response-serialization fallback could only be reached through parser output, so it was removed along with parsers.
There was a problem hiding this comment.
Update: my earlier reply is outdated. 61b4f9e brings back user code on the unit (data_parser), so the containment is back as well. If response_msg.serialize() fails, the worker now replies GET_ERROR (or PUT_GET_ERROR for other operations) and keeps running. I checked that without it, the unit stops answering and the request times out. Covered by test_storage_unit_get_data_parser_parses_each_field_without_changing_stored_values: a parser returning generators gets GET_ERROR, and a plain get on the same unit still succeeds.
| if not 0 <= index <= value.size(0): | ||
| raise IndexError(f"prefix length {index} is out of range for dim 0 of size {value.size(0)}") | ||
| return value[:index] | ||
| if isinstance(index, slice | tuple): |
There was a problem hiding this comment.
Preserve tuple indexes over the parser RPC
When the advertised multidimensional form is used via kv_batch_get, such as parser_args=[(slice(None), slice(0, 2))], the untyped msgpack transport encodes the tuple as an array and decodes it as a list. The storage unit therefore reaches the explicit list-rejection branch rather than this tuple branch, so tuple indexing works only in the direct unit tests and always fails over the actual RPC; preserve a tuple marker during serialization rather than conflating it with intentionally rejected list indexing.
AGENTS.md reference: AGENTS.md:L17-L18
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Option A: done in f15dd10 (woa cherry-pick 1b61e9b).
slice_tensor now restores msgpack-decoded list-with-slice shapes back to an indexing tuple so multi-dim RPC parser_args still slice correctly, while pure int lists stay rejected. Covered by test_slice_tensor_restores_indexing_tuple_after_zmq_roundtrip.
There was a problem hiding this comment.
Superseded by 82ea7f9. slice_tensor and _indexing_tuple_from_list are gone. select_items entries are a slice or a list of indices, and both round-trip through msgpack unchanged, so there's no tuple to restore.
There was a problem hiding this comment.
Update for 61b4f9e: there are still no built-in parsers, so no tuple marker is needed. data_parser itself goes through cloudpickle. parser_args are encoded like other request data, so tuples arrive as lists; the kv_batch_get docstring says so.
| Fetches tensors using the provided metadata, reconstructs them with the | ||
| correct shapes and dtypes, and merge them as a TensorDict according to metadata. | ||
| """ | ||
| if parser is not None: |
There was a problem hiding this comment.
Reject parser_args without a parser on KV backends
When the low-level client is used with Mooncake, Yuanrong, or Ray and supplies parser_args while leaving parser=None, the new argument reaches this method but the guard checks only parser, so the backend fetches and returns the full values while silently ignoring the arguments. The high-level and SimpleStorage paths explicitly reject this combination through validate_get_parser; apply the same validation here so a caller typo cannot look like a successful parsed read.
AGENTS.md reference: AGENTS.md:L17-L18
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Option A: done in f15dd10 (woa cherry-pick 1b61e9b).
KVStorageManager.get_data now calls validate_get_parser before the NotImplemented check, so parser_args without a parser raise ValueError instead of being silently ignored. Covered by test_get_data_parser_args_without_parser_raises.
There was a problem hiding this comment.
Superseded by 82ea7f9. There is now a single select_items argument, so nothing can be passed without its counterpart. KVStorageManager.get_data raises NotImplementedError for any non-None select_items before fetching (test_get_data_with_select_items_raises_before_fetching).
There was a problem hiding this comment.
Update: my earlier reply is outdated. 61b4f9e reintroduced a separate parser_args, and this case came back with it. Fixed in e7bc18d: TransferQueueClient.(async_)get_data raises ValueError("parser_args requires data_parser"), and also checks that parser_args matches the sample count. It does this before calling the manager, so KV backends and SimpleStorage behave the same way. KVStorageManager.get_data still raises NotImplementedError for any data_parser. Covered in test_async_get_data_passes_data_parser_only_when_set and the KV test test_get_data_rejects_data_parser.
Restore RPC indexing tuples after msgpack list decode, omit parser kwargs for old managers, contain serialize failures so the worker survives, and validate KV parser_args without a parser.
|
[AI Review] Thanks for this. The placement is right: trimming runs on the storage unit (data plane), the manager only shards Verification
Must fix
Suggested
Reliability notesChecked and fine: |
| @@ -73,7 +73,8 @@ In the data plane, we utilize a pluggable design, enabling TransferQueue to inte | |||
| Specifically, we provide a `StorageManager` abstraction class that defines the core APIs as follows: | |||
|
|
|||
| - `async def put_data(self, data: TensorDict, metadata: BatchMeta) -> None` | |||
There was a problem hiding this comment.
We can also align the put_data related parser in README
There was a problem hiding this comment.
Done in a5c6205. The README now lists put_data(self, data, metadata, data_parser=None) and get_data(self, metadata, select_items=None), matching StorageManager.
There was a problem hiding this comment.
Update for 61b4f9e: the README now lists get_data(self, metadata: BatchMeta, data_parser=None, parser_args=None), next to put_data(self, data, metadata, data_parser=None).
|
[AI Review] Follow-up to the review above: one design point (how
|
| Hook | Signature | Replayed on timeout |
|---|---|---|
put data_parser |
dict -> dict, whole batch, lengths unchanged |
no (L501-L502) |
update merge_fn |
(old, new) -> value |
no (L581-L583) |
get parser (this PR) |
(value, arg) -> value |
yes (L812-L827) |
What the capability really depends on is the backend, not get: only SimpleStorage units can run code next to the data, which is why KV backends have to raise.
Suggested direction
Which design fits depends on the actual need:
-
Return part of a value, which is what both built-ins do. Make it a declarative option of the get request, next to
select_fields, which already projects columns: for example a per-keyint | slicefor tensor fields and an index list for list fields. Naming and exact shape are your call; load-balanced CP, where each rank takes two non-adjacent chunks, still fits as a short list of slices per key. The request then carries data instead of code:- it cannot mutate storage, and a replayed attempt is harmless;
- the storage unit needs no cloudpickle or importable module;
- plain
int/slice/ index lists round-trip through the existing encoder without the tuple-vs-list problem.
Items 1, 2, 3 and 5 of the earlier review go away,
get_parser.pyshrinks to argument validation, and the storage-unit side becomes a few lines of slicing in_handle_get. -
Run arbitrary user code next to the data. This is a general capability. It should share one contract with
data_parserandmerge_fn: where it runs, no in-place mutation, no replay, and how errors are wrapped. Get would be one call site, and the hook should not be namedget_parser. This deserves its own design discussion. -
The consumer layout is fixed (for example, a CP rank always reads the same shard). Split at put time into per-shard keys or fields; a plain get then fetches exactly what is needed, on every backend, with no storage-side compute. The cost is that producers must know the consumers' parallel layout.
For this PR, option 1 looks like the best fit. It covers exactly the two built-in parsers and is a stronger form of the "built-in parsers only" suggestion in item 1 of the earlier review. Arbitrary storage-side code can follow later, on top of an agreed contract, if a caller needs it.
Tests can shrink to under half
The PR adds about 630 test lines for about 460 source lines. tests/test_get_parser.py alone has 322 lines and 31 test functions, and the e2e class has 8 tests, each run in sync and async mode. For a feature that mostly adds an interface option, most of this volume tests machinery that the earlier items suggest removing.
Goes away with the code it tests:
- Tensor-arg coercion: 7 tests (
keeps_plain_int_list_and_slice_args,coerces_scalar_length_for_slice_tensorwith 5 cases,coerces_1d_lengths_to_list_intwith 4 cases,rejects_bad_tensor_shapes_early,coerces_0d_tensor_as_whole_args_for_one_key,does_not_coerce_select_list_items_args,custom_callable_skips_length_coerce). A single test of the.tolist()conversion replaces them. - Tuple indexing over RPC:
restores_indexing_tuple_after_zmq_roundtrip,rejects_index_list_unlike_select_list_items,noncontiguous_slice_encodes_correct_values, and the tuple half ofaccepts_slice_and_tuple_indexes. - The single-field
select_fieldsrule: the twocheck_parser_select_fieldstests, plus the e2erequires_select_fields_for_multi_field. Once the rule no longer depends on the partition's fields, this becomes one validation case that runs without Ray. - The
parsedflag:test_get_with_parser_rejects_reply_without_parsed_flag. _unwrap_non_tensor:test_apply_get_parser_unwraps_non_tensor_values.- Repeated validation: the KV
test_get_data_parser_args_without_parser_raisesduplicatestest_validate_get_parser_rejects_args_without_parser.
Duplicates regardless of design:
- e2e
selects_video_frames_mixed_with_imagesruns the same path asselects_list_items_per_key:select_list_itemsover a list of tensors, with each row packed as a non-tensor list either way.empty_selectioncan be one moreparser_argscase of the latter. - e2e
out_of_range_index_raisesandexception_reaches_callerboth cover error propagation; keeping one is enough. prefix_returns_contiguous_view,empty_prefix_is_valid_viewandapply_get_parser_slice_tensor_keeps_per_key_viewsall assert the same view/data_ptrproperty.- Once the loop moves into
_handle_get, the fourapply_get_parserunit tests can become one storage-unit test through the existingMockStorageClient.send_get.
Worth keeping: the per-unit parser_args routing test in the manager, the KV NotImplementedError test, the e2e missing-key and tensor-trim tests, the serialize-failure test on the storage unit, the old-manager test in the client, and slice_tensor_prefix_encode_sends_only_sliced_nbytes (the only test that shows the bandwidth gain).
Missing:
- A no-replay test for parser gets. It can mirror
test_put_retries_only_without_a_parserwith one more parametrized case. - If callables stay and a mitigation such as a shallow copy is adopted, a memory-path test showing that a parsed get leaves stored values unchanged. SSD-backed values cannot catch this, because they are decoded fresh on every read.
|
Thanks. I reproduced the three must-fix items on a real SimpleStorage setup before changing anything:
I went with option 1 from the follow-up. In 82ea7f9, the request now carries data instead of code: Your review, item by item:
On the follow-up:
Against |
Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
Restore RPC indexing tuples after msgpack list decode, omit parser kwargs for old managers, contain serialize failures so the worker survives, and validate KV parser_args without a parser. Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
…items Running a user callable on the storage unit let a parser mutate stored values in place (memory-resident ones only, so inconsistently with SSD offload), replayed it on every get retry, and lost tuple indexes over msgpack. The two built-in parsers only ever returned part of a value, so the request now carries that selection as data instead of code. select_items gives one entry per key: a slice, or a list of indices, along dim 0 of every selected value. Units apply it right after loading, which cannot change storage, so retries stay harmless and slices/int lists round-trip unchanged. The manager normalizes tensors, numpy ints and lengths once; select_fields is required, every key must exist, and kv_batch_get_by_meta forwards it. KV backends still raise NotImplementedError. Removed with the callables: utils/get_parser.py and its public exports, the coerce helpers, the parsed reply flag, NonTensor unwrapping, and the response-serialization fallback that only parser output could reach. Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
fe8341d to
47826f6
Compare
|
[AI Review] Re-review of Thanks, this is a clear improvement. I checked every item in your reply against the code and they hold: the request now carries data instead of code, so in-place mutation, parser replay and the tuple-over-RPC problem are gone by construction, and keeping the default retry policy is right because a selection has no side effects. Validation runs once in the manager, Verification
Should fix
Nits
With item 1 fixed, this looks ready to merge from my side. |
This reverts commit 47826f6.
… select_items" This reverts commit 82ea7f9.
…a_parser select_items applied one dim-0 selection to every selected field, which is too rigid when columns need different handling. kv_batch_get now takes an optional data_parser(data, args) -> dict with the same contract as kv_batch_put's data_parser: each SimpleStorage unit runs it on its share of the batch before replying, and the result must keep the keys and per-field element counts. Optional parser_args are aligned with keys and routed to each unit by batch position. - The built-in get parsers (select_list_items, slice_tensor) are removed. - Gets with a parser are not retried, matching put_data. - Put and get share one output check, _apply_data_parser. - A response that fails to serialize becomes an error reply, so a parser returning an unencodable value no longer stalls the unit's only worker. - The client passes the parser to StorageManager.get_data only when set, so custom managers written as get_data(self, metadata) keep working. - KV backends raise NotImplementedError for data_parser, as for put. Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
parser_args without data_parser was dropped silently by TransferQueueClient.get_data, which only forwards the parser arguments when a parser is set, and by KV backends. The client now rejects it, and checks that parser_args matches the number of samples, so direct client callers get the same errors as kv_batch_get. kv_batch_get keeps only the every-key-found check, which needs the keys. Adds a test that a parser returning a view sends only the view's bytes. Signed-off-by: OutstanderWang <wangweiyanster@gmail.com>
|
@0oshowero0 A design update. It supersedes my previous reply and makes most of the re-review moot. Why a parser again.
Your first review, item by item:
Follow-up.
They also add the two you listed as missing: a no-replay test, and the memory-path test that stored values stay unchanged. Re-review. Boolean masks, slice steps, Against |
data_parser to kv_batch_get
data_parser to kv_batch_getdata_parser in kv_batch_get for SimpleStorage
… before batch writes (#196) ## Problem Two key-validation gaps in the batch KV APIs, both reproducible on `main`: 1. **`kv_batch_get` silently drops missing keys.** The controller leaves keys it cannot find out of the retrieved meta, and `async_kv_batch_get` only raised when *every* key was missing. Asking for `["a", "missing", "b"]` returned 2 rows without an error, so the rows no longer lined up with the requested keys, and nothing told the caller which key was dropped. #191 added a check for this, but only when `parser_args` is given. 2. **Batch writes with duplicate keys commit before they fail.** `KVBatchMeta` rejects duplicate keys, but only when it is built for the return value, after the write: - `kv_batch_put(keys=["d", "d"], ...)` raised `ValueError: Got duplicated keys.`, but `d` had already been stored. - `kv_batch_update(keys=["a", "a"], ...)` raised the same error after the merge had been applied: `a` went from `1` to `21`, the result of the second entry's merge. ## Change 1. **`kv_batch_get`**, and the `_by_meta` variants that call it, now raise `ValueError` whenever some keys are missing, and name them: `1 of 3 keys were not found in partition 'p': ['missing']`. The missing keys are looked up with `kv_retrieve_keys`, on this error path only. This replaces the `parser_args`-only check from #191. 2. **`kv_batch_put` / `kv_batch_update`** reject duplicate keys before any request is sent: `keys must be unique, got duplicates: ['d']`. The sync variants call the async ones, so both are covered. Docstrings are updated. This changes behavior for callers that relied on a partial get returning fewer rows; `transfer_queue/`, `tutorial/` and `recipe/` have no such caller. Each fix is a separate commit. ## Tests - `tests/e2e/test_kv_interface_e2e.py`, sync and async: - `test_kv_batch_get_raises_when_some_keys_are_missing` - `test_kv_batch_put_rejects_duplicate_keys_before_writing`, which also checks that the key is absent from `kv_list` - `test_batch_update_rejects_duplicate_keys_before_merging`, which also checks that the stored value is unchanged - The #191 parser test now expects the new missing-key message. - `python -m compileall -q transfer_queue tutorial tests`, `ruff check` and `ruff format --check` pass; `mypy transfer_queue` reports no new errors compared with `main`. - `python -m pytest -q`: 834 passed, 12 skipped. The 8 errors are in `tests/test_yuanrong_storage_client_e2e.py` and are environment-only (`datasystem` is not installed). - All `tutorial/*.py` scripts run cleanly, and so does every code cell of `tutorial/basic.ipynb`, executed in order. Found while reviewing #191. --------- Signed-off-by: 0oshowero0 <o0shower0o@outlook.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
In RL training with complex parallel strategies (e.g. CP / context parallel), a rank often needs only part of a stored value:
input_idstrimmed to the sample length or the rank's shard;TransferQueue can store many columns, and each one may need different handling. This PR adds an optional
data_parsertokv_batch_get/async_kv_batch_get, the get-side counterpart ofkv_batch_put'sdata_parser. Each SimpleStorage unit runs it on its share of the batch before replying, so only the parsed values cross the network. It is an advanced feature, and its constraints are documented.Usage
API
data_parser(data, args) -> dict.datamaps each selected field to a list with one stored value per sample on that unit.argsholds the matchingparser_argsentries, orNonewithoutparser_args. Each field may be parsed differently. The result is checked the same way as on put: same keys, and the same number of values per field.parser_argsare optional per-key arguments aligned withkeys. They requiredata_parser, a matching length, and every key to exist, because missing keys are dropped from the meta and would shift the arguments onto other keys.TransferQueueClient.(async_)get_datachecks the first two, so direct client callers get the same errors.kv_batch_getchecks that every key was found. All of this runs before any storage request is sent. They are serialized like request data, so tuples arrive as lists.max_attempts=1). A storage-unit timeout raises instead of running the parser again.KVStorageManager(MooncakeStore, Yuanrong, RayStore) raisesNotImplementedErrorbefore fetching anything, as it does for put.Constraints documented in the
kv_batch_getdocstring and the README:tensor.add_(),value.append()).dataholds the stored objects, so the change would persist and reach later readers. Build new values instead; replacing entries ofdatais safe.Design
An earlier revision replaced the parser with a declarative
select_items, after the review. That proved too narrow: it applies one dim-0 selection to every selected field, while real layouts need per-column handling (trim one field, pick items from another, decode a third). This revision goes back to a user callable, but with the same contract as put'sdata_parserinstead of a new per-value one. It handles the review's points as follows:put_data.parser_argsdocuments that tuples arrive as lists.GET_ERROR. Before, the exception escaped and the unit's only worker stopped answering.parser_argsagainst the parser and the sample count in the client, and every-key-found inasync_kv_batch_get. There is noparsedreply flag, no exported parser type, and no built-in parsers. The put and get output checks share one helper,_apply_data_parser.Compatibility
kv_batch_getwithoutdata_parserbehaves as before.TransferQueueClient.(async_)get_datapassesdata_parser/parser_argstoStorageManager.get_dataonly when a parser is set, so custom managers written asget_data(self, metadata)still serve plain gets.Changes
transfer_queue/storage/simple_storage.py:_apply_data_parsershared by put and get (put's inline checks moved here unchanged), applied in_handle_get. Response serialization failures become error replies.transfer_queue/storage/managers/simple_storage_manager.py: routesparser_argsto each unit by batch position. One attempt with a parser.transfer_queue/storage/managers/base.py: the newget_dataarguments; KV backends rejectdata_parser.transfer_queue/interface.py,transfer_queue/client.py: the public parameters. The client validatesparser_args;kv_batch_getrequires every key to exist.README.md:StorageManager.get_datasignature, and an "Advanced: parsing on get" paragraph.Tests
tests/test_simple_storage_unit.py: per-field parsing with per-key args leaves the stored values unchanged; a changed element count fails; an unencodable result returnsGET_ERRORand the unit keeps serving; a parser returning a view sends only the view's bytes.tests/test_async_simple_storage_manager.py: each unit receives theparser_argsof its own samples.tests/test_storage_request_retry.py: a get is retried only without a parser.tests/test_client.py: an old-styleget_data(self, metadata)manager still serves plain gets, andparser_argswithoutdata_parserraises.tests/test_kv_storage_manager.py: KV backends raise before fetching.tests/e2e/test_kv_interface_e2e.py, sync and async: parsing across two storage units with per-key args, stored values unchanged afterwards, and the threeparser_argsvalidation errors.pre-commitandpython -m compileall -q transfer_queue tutorial testspass.python -m pytest -qpasses, with the Yuanrong e2e module excluded locally because of an environment-specificTORCH_LIBRARYregistration error.