Skip to content

--layers maps the shard as N disjoint Metal buffers even when the range spans the whole file (~77x decode slowdown) #845

Description

@IzD680

Summary

When ds4-server is started with --layers, the Metal model map is split into one shared buffer per tensor span instead of a single overlapping mapping. On the same machine, with the same binary and the same model family, this costs roughly 77x in decode throughput.

The restriction appears to be a no-op in terms of what is mapped — the reported span is essentially the entire shard file — so the cost looks like pure mapping fragmentation rather than a smaller working set.

Environment

  • Apple Silicon Mac Studio, 512 GB unified memory, Metal backend
  • Two hosts, pipeline parallelism (--role coordinator / --role worker)
  • --ctx 393216, --threads 8, --prefill-chunk 4096
  • Model split into two shards by layer range

What the log shows

With --layers:

restricting metal model map to layers 0:30 (156 spans, 426.09 GiB tensor span)
...
mapped 436323.72 MiB from offset 5.06 MiB
...
156 disjoint shared buffers across 156 tensor spans

The second half of the split reports the same shape:

153 disjoint shared buffers across 153 tensor spans

The restriction restricts nothing. The shard file is 436,323.72 MiB ≈ 426.10 GiB, and the mapped tensor span is 426.09 GiB — the ~10 MiB difference matches the 5.06 MiB header offset. The engine is laying down the same bytes in 156 pieces instead of one.

Without --layers, on the same machine and the same binary, a single-process run of a full model maps as:

1 overlapping shared buffers

Measured impact

configuration mapping decode
single process, no --layers 1 overlapping shared buffers 10.01 t/s
coordinator, --layers 0:30 156 disjoint shared buffers 0.13 t/s

Same box. Same binary. ~77x.

The network is not involved

Per-token telemetry from the coordinator, repeated identically for every token:

distributed telemetry: request=2 hop=0 layers=31:60 pos=11 tokens=1
  eval=2744.908ms   downstream_wait=0.000ms   forward_send=0.000ms

downstream_wait and forward_send are exactly zero, not merely small. Wire traffic is ~0.11 MiB in / ~0.49 MiB out per decode step. The time is entirely compute:

  • worker, 30 layers: ~2,742 ms/token (σ < 7 ms over 8 tokens)
  • coordinator, 31 layers: ~4,965 ms/token (by difference from 7,707 ms/token total)

That is roughly 91–160 ms per layer per token, on hardware where the same engine runs a whole model at 33 t/s.

Both halves are equally affected, which rules out one bad host.

Not memory, not cold cache

Residency time fell from 58,183 ms to 10,000 ms across a restart (warm file cache) and the decode rate did not change at all. Paging and load time are not the cause.

Reproducibility

Reproduced on two separate dates across five runs, including a --ssd-streaming variant, which did not help:

day 1, 07:20   3 tokens / 1667.469 s  = 0.002 t/s
day 1, 08:45   8 tokens /  266.808 s  = 0.03  t/s
day 1, 08:54   7 tokens /  574.693 s  = 0.01  t/s
day 2, 03:xx   8 tokens /   61.65  s  = 0.13  t/s

The same 156 disjoint … 156 tensor spans line is present in the day-1 logs, so this is not specific to a single launch or to startup ordering (restarting with corrected startup order changed nothing).

Why --tensor-parallel is not a workaround here

--role coordinator rejects a start without --layers:

--role coordinator requires --layers

and --tensor-parallel requires the full model on both hosts rather than per-host shards, so on a sharded deployment there is no way to reach a non-fragmented mapping through the CLI.

Suggested fix

Either coalesce adjacent tensor spans when building the restricted map, or skip the restriction entirely when the requested layer range already covers the whole file — which, per the numbers above, is the common case for a per-shard file.

Minor, separate

The binary exits with rc=0 on argument-validation errors. ./ds4-server --role coordinator -m <shard> --ctx 4096 prints --role coordinator requires --layers and still returns 0, so scripts cannot detect a failed launch by exit status and must grep the log. Two probes cost nothing because validation runs before the single-instance guard and before model load — that part is good.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions