Skip to content

perf(cuda): reduce redundant per-token synchronizations#22

Closed
hai-pilgrim wants to merge 12 commits into
heiervang-technologies:htfrom
hai-pilgrim:perf/reduce-cuda-syncs
Closed

perf(cuda): reduce redundant per-token synchronizations#22
hai-pilgrim wants to merge 12 commits into
heiervang-technologies:htfrom
hai-pilgrim:perf/reduce-cuda-syncs

Conversation

@hai-pilgrim

Copy link
Copy Markdown

Summary

Removes unnecessary CUDA synchronization points in the inference pipeline to reduce per-token latency. Inspired by upstream PR ggml-org#20793 which demonstrated measurable gains by reducing redundant syncs between async operations and graph execution.

All changes are scheduling-only — no computation changes, bit-exact output preserved.

Sync points changed

1. ggml_cuda_mul_mat_id — remove stream sync after H2D copy (line ~2420)

  • Before: cudaMemcpyAsync (H2D) followed by cudaStreamSynchronize, then get_rows_cuda kernel on the same stream
  • After: Removed the sync
  • Why safe: Both the async copy and the subsequent kernel are on the same CUDA stream. CUDA stream semantics guarantee that operations submitted to the same stream execute in order. The sync was blocking the host thread unnecessarily without providing any correctness benefit.

2. ggml_backend_cuda_split_buffer_set_tensor — deduplicate sync loop (lines ~951-953)

  • Before: After issuing N async H2D copies (one per device) all on cudaStreamPerThread, a loop called cudaStreamSynchronize(cudaStreamPerThread) once per device count — syncing the same stream N times
  • After: Single cudaStreamSynchronize(cudaStreamPerThread) call
  • Why safe: cudaStreamPerThread is a per-thread implicit stream. All copies are submitted to the same stream regardless of target device, so one sync drains the entire stream. The N-1 extra syncs were no-ops.

3. ggml_backend_cuda_split_buffer_get_tensor — deduplicate sync loop (lines ~990-992)

What is NOT changed

  • cudaDeviceSynchronize in ggml_cuda_set_peer_access — this is a rare state-transition path for multi-GPU peer access that correctly requires device-wide synchronization
  • cudaStreamSynchronize after D2H copy of ids_host in mul_mat_id (line ~2399) — host code reads the copied data immediately, so the sync is required
  • All buffer interface syncs (set_tensor, get_tensor, memset_tensor, cpy_tensor, clear) — these are called from the generic backend API which expects synchronous completion

Compatibility

  • Single GPU (RTX 3090 / SM86): Fully compatible, primary beneficiary
  • Multi-GPU / split tensors: Split buffer paths still sync correctly (single sync drains all pending copies)
  • CUDA graphs: No changes to graph capture/launch path
  • CPU/GPU hybrid: No changes to cross-backend copy or event-based synchronization

Test plan

  • Run inference with a MoE model (exercises mul_mat_id path)
  • Run inference with tensor splitting across GPUs (exercises split buffer paths)
  • Compare perplexity output before/after to verify bit-exactness
  • Benchmark t/s on RTX 3090 to measure latency improvement

🤖 Generated with Claude Code

marksverdhei and others added 12 commits March 29, 2026 19:03
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Register Qwen2_5OmniThinkerForConditionalGeneration architecture for text
and mmproj GGUF conversion. Handle config structure difference where the
Thinker-only variant has vision/audio configs at the top level. Add pooling
type detection for embedding use cases. Fix audio tensor routing to base
MmprojModel class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eiervang-technologies#6)

* docs: add ht-fork documentation, branding, and discussion links

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* convert: support LoRA conversion for MLA kv_b_proj

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add fork sync automation

* feat: add --remap-developer-role flag to translate developer→system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: support LCO-Embedding-Omni (Qwen2.5 Omni Thinker) GGUF conversion

Register Qwen2_5OmniThinkerForConditionalGeneration architecture for text
and mmproj GGUF conversion. Handle config structure difference where the
Thinker-only variant has vision/audio configs at the top level. Add pooling
type detection for embedding use cases. Fix audio tensor routing to base
MmprojModel class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add ht branch to flake8 lint workflow triggers

* feat: welcome agentic contributions, remove upstream AI restrictions

- Delete AGENTS.md (upstream's anti-AI contributor guidelines)
- Replace restrictive AI Usage Policy with welcoming Agentic Contributions section
- Update README to highlight fork's pragmatic stance on AI contributions

Unlike upstream, we evaluate code by quality, not by how it was written.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…hnologies#8)

* webui: add cancel button for in-progress model loading

Allow users to cancel a model that is stuck loading or taking too long
in the router mode model selector. The cancel button appears next to
the loading spinner in both the model selector dropdown/sheet trigger
and within individual model option rows.

Uses the existing /models/unload endpoint which already supports
unloading models in LOADING state. The frontend polling loop is
interrupted via AbortController to prevent stale error toasts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* webui: add cancelling state indicator and fix cancel polling

- Show orange "Cancelling" indicator with spinner while cancel is in progress
- Poll until server confirms model is no longer in LOADING state before
  clearing the cancelling indicator
- Guard against redundant unload calls on already-unloaded models
- Keep loadingModelId alive during cancel so selector trigger shows
  the cancelling state correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(webui): color-coded spinners for model load/unload/cancel states

- Loading: green spinner, clockwise
- Unloading: red spinner, reverse direction with "Unloading" label
- Cancelling: orange spinner, reverse direction
- Track unloading state separately in models store

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webui): address PR review feedback for cancel model loading

- Remove duplicated cancel logic from ModelsSelector and ModelsSelectorSheet
  by deriving loading/cancelling state from the store (issue heiervang-technologies#1)
- Fix race condition: no longer set isLoadingModel=false before cancel
  completes, preventing brief UI flash (issue heiervang-technologies#2)
- Add MAX_CANCEL_POLL_ATTEMPTS (60) timeout to cancel polling loop
  to prevent infinite polling if server never transitions (issue heiervang-technologies#3)
- Replace div cancel buttons with proper <button> elements for
  keyboard accessibility and screen reader support (issue heiervang-technologies#4)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…eiervang-technologies#10)

- Rename all frontend references from "llama.cpp" to "ht-llama.cpp"
- Dark mode: turquoise-tinted backgrounds, purple-tinted text
- Light mode: inverted — turquoise backgrounds, purple text
- Add reverse spin animation utility class

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…eiervang-technologies#14)

Auto-discover LoRA adapters from the models directory by scanning GGUF
metadata (general.type = "adapter") and match them to models by
architecture. Adapters are loaded with --lora-init-without-apply so
they start disabled and can be toggled on via the UI.

Frontend adds a Popover-based LoRA dropdown in the chat action bar
(next to model selector) with multi-select checkboxes and scale inputs.
Includes "Show only matching" toggle to view all discovered adapters.
Works in both MODEL and ROUTER mode.

Backend changes:
- Add GGUF metadata scanning for adapter classification (preset.cpp)
- Auto-inject matching LoRA adapters into child process args (server-models.cpp)
- Include discovered adapters in /v1/models response
- Fix router proxy for /lora-adapters POST (array body fallback to query param)

Frontend changes:
- New LoraAdapters popover component with checkbox multi-select
- LoRA service with router mode support (query param routing)
- Reactive store with toggle, scale, change tracking, apply
- Integration in ChatFormActions bar and chat completion requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unnecessary CUDA synchronization points that add latency without
providing correctness benefits:

1. mul_mat_id: remove cudaStreamSynchronize after H2D async copy of
   sorted IDs buffer - the subsequent get_rows_cuda kernel runs on the
   same stream, so CUDA stream ordering already guarantees completion.

2. split_buffer_set_tensor: replace N redundant cudaStreamSynchronize
   calls (looped once per device count) with a single sync - all async
   H2D copies use the same cudaStreamPerThread.

3. split_buffer_get_tensor: same fix as above for D2H copies.

These are scheduling-only changes with bit-exact output. Safe for
multi-GPU (split buffer sync still waits for all copies) and single-GPU.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@marksverdhei

Copy link
Copy Markdown

Closing — head branch was deleted from remote. Reopen with a fresh branch if the work is still relevant.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants