Skip to content

[perf] Pack serially in YuanrongStorageClient (DS_MAX_WORKERS 16 -> 1) - #153

Merged
0oshowero0 merged 1 commit into
Ascend:mainfrom
Chase-Rong:serial-packing-yuanrong
Aug 12, 2026
Merged

[perf] Pack serially in YuanrongStorageClient (DS_MAX_WORKERS 16 -> 1)#153
0oshowero0 merged 1 commit into
Ascend:mainfrom
Chase-Rong:serial-packing-yuanrong

Conversation

@Chase-Rong

Copy link
Copy Markdown
Contributor

Problem

batch_encode_into only parallelises pack_into; the msgpack encoding itself is
always serial. pack_into is a memcpy of a few hundred bytes per object — at TQ's
object sizes it is dominated by Python/ATen dispatch, which holds the GIL. Handing
that to a ThreadPoolExecutor does not overlap anything; the threads serialise on the
GIL and the pool's per-task cost is added on top.

Measured on a 512-sample put (1024 objects), Ascend NPU container:

value
actual work (same code, 1 worker) 51.6ms
pack phase wall clock, 16 workers 181.8ms
16 workers vs serial 3.52x slower
per-call wall clock, 1 worker 50.4us
per-call wall clock, 16 workers 1942us (38.5x inflation)

The per-call inflation is the tell: the same function on the same input takes 38.5x
longer per invocation once 16 threads contend for the GIL. Effective parallelism is
~10.9x, but each call is 38.5x slower, so the phase ends up 3.5x slower than just
doing it in order. At 1024 samples the picture is identical: 104.7ms of work takes
360.0ms across 16 threads (3.44x slower).

Change

DS_MAX_WORKERS: int = 16 -> 1, which takes batch_encode_into down its existing
serial path. It stays a class attribute, so a deployment that measures a different
trade-off can override it.

Measurements

verl GRPO on main at 750719e, this PR applied alone (the other two perf branches
reverted to baseline). Medians over 4-7 calls; two independent baseline runs per
configuration:

baseline this PR
single-node, pack phase 188.9 / 192.5ms 51.4ms 3.71x
single-node, TQ-side total 269.5 / 275.3ms 133.2ms 2.05x
single-node, put_data 316 / 319ms 183ms 1.73x
dual-node, pack phase 364.5 / 367.9ms 104.8ms 3.49x
dual-node, TQ-side total 530.8 / 529.9ms 269.7ms 1.97x

yuanrong RPC time is unchanged (32.1 -> 31.4ms single-node, 55.2 -> 61.3ms dual-node),
so the saving is entirely client-side CPU. get_data is unaffected as expected
(medians within 3% across variants).

Two notes on reading these numbers:

  • The reported tq_ms component rises (269.5 -> 133.2 is the TQ-side total, but the
    pack_into label goes from 12.0 to 51.4ms) because packing stops being spread
    across threads and becomes real serial time. The phase wall clock is what drops.
  • Dual-node put_data wall clock is not a reliable metric in this environment: an
    intermittent ~480ms run_in_executor handoff delay appears in some runs
    (two baseline runs measured 606ms and 1083ms with every instrumented step within
    1%, the difference landing entirely in un-instrumented time). Judge the dual-node
    effect by TQ-side total or pack phase, which agree to 0.2%/0.9% between those two
    runs.

How the ratio scales

The ratio is flat in object count and decays in object size — it does not grow in
either direction:

objects (512B each) 512 1024 2048 4096 8192
16w vs serial 3.94x 3.77x 3.48x 3.52x 3.56x

Both paths scale linearly with count (8x the objects gives 7.6x / 8.05x the time), so
growing the cluster or the global batch increases the absolute saving
proportionally — 137ms at 1024 objects, 260ms at 2048 — while leaving the ratio
unchanged. Object size is the only axis that erodes the ratio, per the table above:
3.6x at 512B, 2.6x at 32KB, break-even at 256KB, and a net loss beyond that.

In short: this optimisation is sized by how big each object is, not by how many.
TQ's one-key-per-(sample, field) layout keeps objects in the 512B-8KB range, which is
where the serial path is furthest ahead.

Applicability: this wins because TQ's objects are small

The saving comes from removing GIL contention, whose cost is roughly fixed per
map() call, while the work being parallelised grows with object size. So the win
shrinks as objects get bigger, and eventually reverses. Swept on the same machine,
object count fixed at 1024:

object size 16 workers 1 worker 16w vs serial serial us/obj
512B 147.1ms 40.7ms 3.62x slower 39.7
2KB 151.7ms 40.7ms 3.73x slower 39.7
8KB 145.3ms 43.7ms 3.33x slower 42.6
32KB 154.9ms 58.6ms 2.64x slower 57.3
128KB 178.2ms 115.3ms 1.55x slower 112.6
256KB 205.3ms 199.1ms 1.03x — break-even 194.4
512KB 244.5ms 362.0ms 0.68x — serial loses 353.5

The mechanism is visible in the two middle columns: across a 1024x range of object
sizes the 16-worker wall clock moves only 66% (147 -> 245ms), because it is dominated
by a fixed contention cost, while the serial wall clock tracks real work and grows
8.9x. The ratio narrows because serial catches up, not because the threads get better.
Past ~256KB the memcpy is long enough to release the GIL productively and the pool
starts paying off.

Where TQ sits today: keys are global_index@field_name, one key per (sample,
field), so objects are small. The production put measured in this run costs 50.4us per
object serially, which lands between the 512B and 8KB rows above — i.e. in the flat
region where serial is 3.3-3.7x ahead, far from break-even.

By object count the ratio is stable, so this does not decay as the cluster or the
global batch grows (512B objects, 1 worker as reference):

objects 512 1024 2048 4096 8192
16w vs serial 3.94x 3.77x 3.48x 3.52x 3.56x

Both paths scale linearly (8x the objects gives 7.6x / 8.05x the time), so a larger
batch increases the absolute saving proportionally and leaves the ratio intact.

@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: 5c6285c089

ℹ️ 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".

PUT_KEYS_LIMIT: int = 10_000
GET_CLEAR_KEYS_LIMIT: int = 10_000
DS_MAX_WORKERS: int = 16
DS_MAX_WORKERS: int = 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve parallel packing for large CPU values

When Yuanrong stores general CPU-side values whose packed buffers are large, such as image-sized CPU tensors/ndarrays sent through kv_batch_put, forcing the default to one worker disables the existing parallel pack_into path for every batch. These values are accepted by the public KV/general-object path, and the commit's own size sweep shows serial packing becomes slower past roughly 256 KiB per object (about 1.5x slower at 512 KiB), so an adaptive threshold or configurable default would keep the small-object win without regressing supported large-value workloads.

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

Useful? React with 👍 / 👎.

@0oshowero0

Copy link
Copy Markdown
Collaborator

CC @dpj135

@0oshowero0
0oshowero0 merged commit bc222a4 into Ascend:main Aug 12, 2026
7 of 8 checks passed
@ascend-robot

Copy link
Copy Markdown

CLA Signature Guide

@Chase-Rong , thanks for your pull request.

The following commit(s) are not associated with a signed Contributor License Agreement (CLA).

Commit Reason
[5c6285c [perf] Pack serially in Yuanron...](5c6285c) the email used in the commit is not linked to a signed CLA!
please verify that it matches the email you used when signing the CLA.

To sign CLA, click here.

To check if your email is configured correctly, refer to the FAQs.

Once you've signed the CLA or updating your email, please comment /check-cla to revalidate CLA status.

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.

3 participants