Repository navigation
sycl: add set_tensor_2d for faster tensor-split loading - #30358
Conversation
Pack host rows into chunks of up to 64 MiB when destination rows are contiguous, avoiding a separate upload call for each row. Keep the existing per-row copy path for non-contiguous destinations. On two Arc Pro B70s with gpt-oss-120b UD-Q4_K_XL, total llama-perplexity runtime drops from 263-268 s to 114-115 s. A full-load readback check verified 26,784,256 rows with no mismatches.
arthw
left a comment
There was a problem hiding this comment.
It's good job!
The running time is reduced from 35s to 27s on 2xArc770 by the cmd:
time ./build/bin/llama-perplexity -sm tensor -ngl 99 -c 512 --chunks 4 --load-mode dio -m ../models/Meta-Llama-3.1-8B-Instruct-Q4_K_S.gguf -f README.md
Thank you!
| for (size_t i = 0; i < n; ++i) { | ||
| memcpy(host_buf.data() + i * size, src + (i0 + i) * stride_data, size); | ||
| } | ||
| SYCL_CHECK(CHECK_TRY_ERROR(stream->memcpy(dst + i0 * size, host_buf.data(), n * size).wait())); |
There was a problem hiding this comment.
In the first for(), call wait() for every memcpy() is not necessary.
Only need call wait() on once time out of the first for(), to reduce the performance impact.
There was a problem hiding this comment.
Thanks for reviewing and testing this on the A770s!
I updated the implementation to reuse the pinned staging slots already used by set_tensor. The original version reused a single buffer, so removing its' wait would let the CPU overwrite data while the device was still reading it.
With the staging slots, it only waits before reusing a slot, allowing packing and device copies to overlap.
On my two B70s with gpt-oss-120b, this reduced the time in set_tensor_2d from 17–18 seconds to 9–9.5 seconds, including waiting for the copies to finish. Total runtime dropped from 73–76 seconds to 64–67 seconds.
I also tested a single ext_oneapi_memcpy2d call, but it was considerably slower on this setup: around 227–229 seconds for the copies. The updated staging implementation passed a full readback comparison of 26.8 million rows with zero mismatches.
Please let me know if you have any other questions or requests. Hope this helps the community!
Implement set_tensor_2d to speed up tensor-split model loading by packing rows into the existing pinned staging slots and uploading each batch with a single device copy. Share staging initialization with set_tensor and wait only when reusing a slot, allowing CPU packing and device copies to overlap. Fall back to per-row uploads for noncontiguous device rows, oversized rows, or unavailable staging.
|
Hi all - Anything else you need here? I have other PR's with additional improvements I'd like to submit when this one is approved or rejected. |
|
Tested with 2 - B70's loading a 35GB and 52GB LLM. Deltas are repeatable across multiple test runs with minor variance. llama-perplexity -m Qwen3.8-27B-UD-Q8_K_4XL.gguf -dev SYCL0,SYCL1 -sm tensor -ngl 99 -c 512 --chunks 4 --load-mode dio -v
llama-perplexity -m Qwen3.8-27B-BF16/Qwen3.8-27B-BF16-00001-of-00002.gguf -dev SYCL0,SYCL1 -sm tensor -ngl 99 -c 512 --chunks 4 --load-mode dio -v
Test System: @Captain-Tripps - Thanks much. Added to my daily driver. |
arthw
left a comment
There was a problem hiding this comment.
It's good job!
I tested on Arc770, running time is decreased.
Thank you!
|
i tested this with my 2x B70 and qwen3.8-flash-next IQ3_XS. |
|
@moriz1 -sm tensor favors token generation while -sm layer favors prompt eval with dual GPUs irrespective of this patch. I measure a starting 300 token/s delta between file ingest between tensor=860 ingest/faster tg and layer=1160 ingest/slower tg. This is a bus/memory speed issue between multiple GPUs ( and the code that does the data exchange ). I do not believe the B70 exposes direct P2P data exchanges. |
|
@92es by poor performance, I meant that token generation has gone DOWN with Like I alluded to earlier, this is likely a separate issue. |
|
I have just the other problem, but I am using a dense LLM across 2 B70s not the Flash-Next structure. I am testing a small patch to ggml_sycl_comm_exchange() that brings split tensor ingest t/s on par with split layer ingest t/s. My tensor 860 t/s is now 1050 t/s, not quite the full 1160, but I closed 200:300 t/s gap between the 2. This depends on your PCIe bus speeds. I have a Gen4x4 bottleneck that I am trying to make best use of the bandwidth. Sneak Preview: Update: I unit tested the [4 buffer memcpy/wait/memcpy] pattern against the [2 buffer event/memccpy/wait] pattern before making the patch. The Gen5x16 to Gen4x4 pathway went from 5.3 GB/s ( mainline code ) to 7.1 GB/s ( +34%, patch code ), almost maxing out the theoretical limit of 7.8.GB/s. |
|
@Captain-Tripps , can you check out #30956 ? I do not want it to step on your #29604 quad B70 config. |
@92es - I tested #30956 on Intel Arc Pro B70 (SYCL, Windows 11, 2 cards via ONEAPI_DEVICE_SELECTOR=level_zero:0,1). Caveat: this was a hand port onto our own SYCL branch (cherry-pick conflicts), not this branch as-is. Only ggml-sycl.dll differed between the baseline and the PR build. Overall - Your PR is a clear win for tensor-split prompt processing on SYCL. Details: Qwen3.8-27B Q8_0: 4.8336 At larger batch sizes the values stay within the baseline’s own run-to-run spread. Speed, 2-card -sm tensor, baseline → PR (server, about 33k-token prompt, A/B/B/A order, mean of 2 runs): model prompt read decode Qwen3.8-27B, layer vs tensor+PR (131k context, q4_0 KV, MTP drafting with --spec-draft-n-max 4): model split decode (MTP) 33k prompt read decode after it This matches what you measured: Qwen3.8-27B prompt reads in tensor split land roughly level with layer split. With MTP drafting, though, tensor split decodes about 18% slower on 2 cards. I didn’t run tensor without the PR for this model, so I can’t say how much of that gap the PR closes. Layer split + MTP is still the winning recipe on my machine and we've yet to unpack why. My guess is the per-layer exchange on each small draft/verify step, but that’s unmeasured. |
|
@Captain-Tripps , Is there a stable way to test MTP in llama-bench or llama-perplexity ( I am very knew at llama.cpp, but have coded and tested once or twice before ) ? When I A/B test in llama-server with Twin B70s, MTP, 292K KV BF16 context having it read the same largest source code file on startup, ingest jumps from 860 - 1100 so ingest speedup is still present with MTP. What is the a stable repeatable way to test MTP after the ingest ? When it is doing subsequent thinking, the modified pathway is still being called. If I have a stable way to A/B the MTP, I can chase after the cause of the delta. Thanks much. |
|
@92es I’d split this into two measurements:
Repeat with -sm layer. This prints decoded tok/s plus n_drafted, n_accept, and acceptance %. The fixed prompt + --temp 0 should make it deterministic for a given build and split mode. The important bit is to compare acceptance alongside tok/s. Tensor splitting changes reduction order slightly, which can change logits and therefore MTP acceptance. So an end-to-end tok/s difference can be either verify-step cost, acceptance, or both. This should also be a useful check for the two-buffer change: compare p=1 and p=5 first. My current suspicion—based on code reading, not measurement—is that decode is dominated by the host waits in each allreduce, rather than the copy bandwidth. If that is right, the patch may help prompt throughput more than decode/MTP. I have not run the speculative-simple side on Zeus yet, so I’d treat that last part as a hypothesis to validate. |
|
I think I may have a fix. Need to test more, but acceptance is now identical (mainline vs patch) down to 4 levels using single-shot llama-cli with MTP runs.
Maybe able to have cake and eat it too !!! I changed the (FIRST PAIR ONLY, not last pair) waits to ext_oneapi_submit_barrier to change how blocking behavior is done: Before:After:Using:Will be rerunning my entire llama-perplexity regression matrix in addition and updating #30956 accordingly. Stock Run:Patch Run:Update:
|
|
@92es - Nice. Will be interested to see how it goes. Let's move this collab out of this PR so @arthw can get this merged with main and we don't clutter it up. I have a public repo where I'm updating my hill-climbing efforts on B70's. That's where my PR's here stem from. https://github.com/Captain-Tripps/b70-lab/blob/main/llama.cpp-sycl/TENSOR-SPLIT-DECODE.md is the one I'm looking at now for tensor split. Point your clanker at https://github.com/Captain-Tripps/b70-lab and lets see what we can come up with. |
Overview
SYCL currently leaves set_tensor_2d unset, so uploads through ggml_backend_tensor_set_2d fall back to a separate set_tensor call for every row. In tensor-split mode (-sm tensor), that adds a lot of overhead when loading model weights.
This adds a SYCL implementation that packs the source rows into a host buffer and copies them in chunks of up to 64 MiB when the destination rows are contiguous. The limit keeps the temporary host allocation bounded. If the destination rows aren't contiguous, it uses the existing per-row copy path.
I tested this on two Arc Pro B70s with gpt-oss-120b UD-Q4_K_XL, comparing master f2918ca against the same commit with this change:
llama-perplexity -sm tensor -ngl 99 -c 512 --chunks 4 --load-mode dioAcross two runs, total runtime dropped from 263-268 seconds to 114-115 seconds. Earlier runs put the time to the first perplexity pass, which is mostly model loading, at roughly 256 seconds on master versus 110 seconds with the change.
For a direct copy check, I used a test build that read each uploaded chunk back and compared it with the source rows. One full model load checked 26,784,256 rows with no mismatches. Perplexity results were within the reported uncertainty.
The full test-backend-ops suite hits the same assertion in element_wise.cpp on both master and the patched build, just after the GEGLU_QUICK cases. That suite doesn't exercise set_tensor_2d; the readback comparison above checks the new upload path directly.
The branch was rebased onto f2cc728. There were no SYCL changes between that commit and this tested base, and the patch is unchanged.
Additional information
Requirements