Skip to content

feat(server): add Gemma4 target-layer-split adapter#273

Merged
davide221 merged 8 commits into
Luce-Org:mainfrom
weicj:feat-cpp-server-gemma4-layer-split-adapter
May 28, 2026
Merged

feat(server): add Gemma4 target-layer-split adapter#273
davide221 merged 8 commits into
Luce-Org:mainfrom
weicj:feat-cpp-server-gemma4-layer-split-adapter

Conversation

@weicj

@weicj weicj commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds Gemma4 support to the C++ server target-layer-split path. Gemma4 can now use the same LayerSplitBackend adapter flow as Qwen, so same-backend multi-GPU target placement is a reusable server capability rather than a Qwen-only path.

Changes

  • Routes arch=gemma4 + --target-devices into a new Gemma4LayerSplitAdapter, reusing the shared LayerSplitBackend introduced by feat(server): add target-layer-split backend adapter path #265.
  • Adds Gemma4 partial GGUF loading with LayerSplitLoadPlan: each shard uploads only its owned block tensors, while the final shard owns output norm / lm-head tensors.
  • Keeps CPU token embedding available on every shard without uploading non-owned target weights to that shard GPU.
  • Adds partial Gemma4 KV cache allocation and validates Gemma4 KV-sharing boundaries before serving.
  • Adds the Gemma4 per-layer split forward path: shard-to-shard activation transfer, per-layer embedding injection support, and SWA ring-boundary step splitting.
  • Adds shard-aware in-memory snapshot / restore so prefix-cache restore works for split Gemma4 targets.

Notes

  • Other model adapters can follow the same adapter shape instead of adding separate backend implementations.
  • Local validation on 2026-05-24: CUDA and HIP ROCm 6.3.3 / dual gfx906 builds passed; runtime smoke passed for Gemma4 21B IQ4_XS split across hip:0,hip:1 with prefix-cache hit, and the existing Qwen3.6 27B target-layer-split path passed the same HIP regression smoke.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 47 files

Re-trigger cubic

@weicj
weicj marked this pull request as draft May 24, 2026 06:28
@weicj
weicj force-pushed the feat-cpp-server-gemma4-layer-split-adapter branch from 01c6842 to c421bc7 Compare May 24, 2026 07:25
@weicj
weicj force-pushed the feat-cpp-server-gemma4-layer-split-adapter branch from c421bc7 to 79abba9 Compare May 28, 2026 06:41
@weicj
weicj marked this pull request as ready for review May 28, 2026 07:07
easel pushed a commit to easel/lucebox-hub that referenced this pull request May 28, 2026
Record the clean integration of PRs Luce-Org#265 and Luce-Org#273, the refreshed conflict probes for the remaining selective-port PRs, and the current validation results.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

8 issues found across 37 files

You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="server/src/common/layer_split_utils.cpp">

<violation number="1" location="server/src/common/layer_split_utils.cpp:75">
P2: Peer-access setup silently ignores pairwise enable failures and always reports success through a `bool` return that never reflects reality.</violation>
</file>

<file name="server/src/qwen35/qwen35_target_graph.cpp">

<violation number="1" location="server/src/qwen35/qwen35_target_graph.cpp:1440">
P2: Snapshot/restore mismatch validation only checks aggregate pair existence with XOR (`(!sk || !sv) != (!dk || !dv)`), so an internally broken pair on both sides silently skips instead of failing. Similar pattern exists for SSM/conv pairs.</violation>
</file>

<file name="server/src/gemma4/gemma4_loader.cpp">

<violation number="1" location="server/src/gemma4/gemma4_loader.cpp:362">
P2: Backend buffer `out.buf` is leaked on the per-tensor allocation failure path</violation>
</file>

<file name="server/src/common/layer_split_backend.cpp">

<violation number="1" location="server/src/common/layer_split_backend.cpp:221">
P2: Destructor calls shutdown() but shutdown() does not clear adapter_, enabling double adapter shutdown if explicitly called before destruction</violation>
</file>

<file name="server/src/placement/placement_config.cpp">

<violation number="1" location="server/src/placement/placement_config.cpp:25">
P2: `layer_split_weights` validation is silently skipped when `layer_split_gpus` is empty, allowing invalid config to pass validation</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/src/common/dflash_draft_ipc_daemon.cpp Outdated
Comment thread server/src/common/layer_split_utils.cpp
: "");
}

if (!dp.layer_split_gpus.empty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: layer_split_weights validation is silently skipped when layer_split_gpus is empty, allowing invalid config to pass validation

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/placement/placement_config.cpp, line 25:

<comment>`layer_split_weights` validation is silently skipped when `layer_split_gpus` is empty, allowing invalid config to pass validation</comment>

<file context>
@@ -0,0 +1,65 @@
+                    : "");
+    }
+
+    if (!dp.layer_split_gpus.empty()) {
+        if (dp.layer_split_gpus.size() < 2) {
+            return "layer_split_gpus must have at least 2 entries";
</file context>

ggml_tensor * dk = cache.attn_k[i];
ggml_tensor * sv = snap.attn_v_snap[i];
ggml_tensor * dv = cache.attn_v[i];
if ((!sk || !sv) != (!dk || !dv)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Snapshot/restore mismatch validation only checks aggregate pair existence with XOR ((!sk || !sv) != (!dk || !dv)), so an internally broken pair on both sides silently skips instead of failing. Similar pattern exists for SSM/conv pairs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/qwen35/qwen35_target_graph.cpp, line 1440:

<comment>Snapshot/restore mismatch validation only checks aggregate pair existence with XOR (`(!sk || !sv) != (!dk || !dv)`), so an internally broken pair on both sides silently skips instead of failing. Similar pattern exists for SSM/conv pairs.</comment>

<file context>
@@ -1428,6 +1437,11 @@ bool restore_target_cache(const PrefixSnapshot & snap, TargetCache & cache) {
         ggml_tensor * dk = cache.attn_k[i];
         ggml_tensor * sv = snap.attn_v_snap[i];
         ggml_tensor * dv = cache.attn_v[i];
+        if ((!sk || !sv) != (!dk || !dv)) {
+            set_last_error("restore_target_cache: KV shard layout mismatch");
+            return false;
</file context>

Comment on lines +362 to +364
set_last_error("gemma4: tensor alloc failed");
mmap.close_map();
gguf_free(gctx); return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Backend buffer out.buf is leaked on the per-tensor allocation failure path

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/gemma4/gemma4_loader.cpp, line 362:

<comment>Backend buffer `out.buf` is leaked on the per-tensor allocation failure path</comment>

<file context>
@@ -254,35 +322,65 @@ bool load_gemma4_gguf(const std::string & path,
+    for (const Gemma4TensorAlloc & a : allocs) {
+        if (ggml_backend_tensor_alloc(out.buf, a.tensor,
+                                      base + a.buffer_offset) != GGML_STATUS_SUCCESS) {
+            set_last_error("gemma4: tensor alloc failed");
+            mmap.close_map();
+            gguf_free(gctx); return false;
</file context>
Suggested change
set_last_error("gemma4: tensor alloc failed");
mmap.close_map();
gguf_free(gctx); return false;
set_last_error("gemma4: tensor alloc failed");
ggml_backend_buffer_free(out.buf);
out.buf = nullptr;
mmap.close_map();
gguf_free(gctx); return false;

return adapter_ && adapter_->supports_remote_draft();
}

void LayerSplitBackend::shutdown() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Destructor calls shutdown() but shutdown() does not clear adapter_, enabling double adapter shutdown if explicitly called before destruction

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/common/layer_split_backend.cpp, line 221:

<comment>Destructor calls shutdown() but shutdown() does not clear adapter_, enabling double adapter shutdown if explicitly called before destruction</comment>

<file context>
@@ -0,0 +1,225 @@
+    return adapter_ && adapter_->supports_remote_draft();
+}
+
+void LayerSplitBackend::shutdown() {
+    if (adapter_) adapter_->shutdown();
+}
</file context>

Comment thread server/src/common/layer_split_utils.cpp
if (!peer_access) return true;
for (size_t i = 0; i < gpus.size(); ++i) {
for (size_t j = i + 1; j < gpus.size(); ++j) {
(void)enable_peer_access_pair(gpus[i], gpus[j]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Peer-access setup silently ignores pairwise enable failures and always reports success through a bool return that never reflects reality.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/common/layer_split_utils.cpp, line 75:

<comment>Peer-access setup silently ignores pairwise enable failures and always reports success through a `bool` return that never reflects reality.</comment>

<file context>
@@ -39,51 +43,70 @@ std::vector<std::pair<int,int>> compute_layer_ranges(
+    if (!peer_access) return true;
+    for (size_t i = 0; i < gpus.size(); ++i) {
+        for (size_t j = i + 1; j < gpus.size(); ++j) {
+            (void)enable_peer_access_pair(gpus[i], gpus[j]);
         }
+    }
</file context>

… backend rollback

- dflash_draft_ipc_daemon: stream get/set_feature_range in bounded 256-token
  chunks so daemon host memory is O(chunk) not O(n_tokens), avoiding large
  transient allocations / OOM on long-context snapshot/restore. Wire bytes and
  client read protocol unchanged.
- layer_split_utils: roll back already-created snapshot backends when
  init_layer_split_snapshot_backends fails partway, fixing a CPU-backend leak.

Co-Authored-By: WOZCODE <contact@withwoz.com>
@davide221
davide221 merged commit fda8877 into Luce-Org:main May 28, 2026
3 checks passed
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 4, 2026
## What

Adds a richer GGUF identity reader on top of Howard Su's existing
`gguf_inspect` (PR Luce-Org#305):

- `GgufMetadata` struct — captures `general.*` + `<arch>.*` header fields
  (architecture, name, file_type, quantization_version, block_count,
  embedding_length, context_length, vocab_size) with -1 / "" sentinels
  distinguishing "not in GGUF" from legitimate zero.
- `read_gguf_metadata(path, compute_sha256)` — best-effort header read;
  optional SHA-256 of the whole file.
- Self-contained SHA-256 mini-impl (RFC 6234) — no OpenSSL dependency
  added for one hash.
- `<path>.sha256` sidecar caching — first server start hashes the file
  (~30s for a 17 GB GGUF on NVMe), subsequent starts read the sidecar.
  Sidecar I/O failures are non-fatal.
- `llama_ftype_name` decode — maps `general.file_type` ints to
  human-readable names ("Q4_K_M", "IQ4_XS", etc.) for /props.

## Why

`/props` schema-4 wants a single authoritative "exactly what binary +
GGUF + quant + sha256 is loaded" payload so benchmarking and provenance
tooling can pin model identity across runs without re-parsing GGUF
headers in every consumer. The sidecar makes the SHA-256 free after the
first boot, which is what makes it usable as a default-on identity
field.

## Dependencies

None. This is purely additive on top of `gguf_inspect.{cpp,h}` as merged
in PR Luce-Org#305 — zero deletions, 333 insertions total. No other server
files or build rules change in this PR; consumers will be wired up
separately.

## Scope note

This PR is the extracted-and-cleaned remnant of the previously closed
PR Luce-Org#336 after a provenance audit; everything else from that branch
(c2_gate, qwen3 drafter changes, structural-defense loaders, and the
inadvertent reverts of Luce-Org#273/Luce-Org#295/Luce-Org#297) is either landing through its
canonical PR (Luce-Org#274) or being dropped entirely.
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 5, 2026
## What

Adds a richer GGUF identity reader on top of Howard Su's existing
`gguf_inspect` (PR Luce-Org#305):

- `GgufMetadata` struct — captures `general.*` + `<arch>.*` header fields
  (architecture, name, file_type, quantization_version, block_count,
  embedding_length, context_length, vocab_size) with -1 / "" sentinels
  distinguishing "not in GGUF" from legitimate zero.
- `read_gguf_metadata(path, compute_sha256)` — best-effort header read;
  optional SHA-256 of the whole file.
- Self-contained SHA-256 mini-impl (RFC 6234) — no OpenSSL dependency
  added for one hash.
- `<path>.sha256` sidecar caching — first server start hashes the file
  (~30s for a 17 GB GGUF on NVMe), subsequent starts read the sidecar.
  Sidecar I/O failures are non-fatal.
- `llama_ftype_name` decode — maps `general.file_type` ints to
  human-readable names ("Q4_K_M", "IQ4_XS", etc.) for /props.

## Why

`/props` schema-4 wants a single authoritative "exactly what binary +
GGUF + quant + sha256 is loaded" payload so benchmarking and provenance
tooling can pin model identity across runs without re-parsing GGUF
headers in every consumer. The sidecar makes the SHA-256 free after the
first boot, which is what makes it usable as a default-on identity
field.

## Dependencies

None. This is purely additive on top of `gguf_inspect.{cpp,h}` as merged
in PR Luce-Org#305 — zero deletions, 333 insertions total. No other server
files or build rules change in this PR; consumers will be wired up
separately.

## Scope note

This PR is the extracted-and-cleaned remnant of the previously closed
PR Luce-Org#336 after a provenance audit; everything else from that branch
(c2_gate, qwen3 drafter changes, structural-defense loaders, and the
inadvertent reverts of Luce-Org#273/Luce-Org#295/Luce-Org#297) is either landing through its
canonical PR (Luce-Org#274) or being dropped entirely.
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 5, 2026
## What

Adds a richer GGUF identity reader on top of Howard Su's existing
`gguf_inspect` (PR Luce-Org#305):

- `GgufMetadata` struct — captures `general.*` + `<arch>.*` header fields
  (architecture, name, file_type, quantization_version, block_count,
  embedding_length, context_length, vocab_size) with -1 / "" sentinels
  distinguishing "not in GGUF" from legitimate zero.
- `read_gguf_metadata(path, compute_sha256)` — best-effort header read;
  optional SHA-256 of the whole file.
- Self-contained SHA-256 mini-impl (RFC 6234) — no OpenSSL dependency
  added for one hash.
- `<path>.sha256` sidecar caching — first server start hashes the file
  (~30s for a 17 GB GGUF on NVMe), subsequent starts read the sidecar.
  Sidecar I/O failures are non-fatal.
- `llama_ftype_name` decode — maps `general.file_type` ints to
  human-readable names ("Q4_K_M", "IQ4_XS", etc.) for /props.

## Why

`/props` schema-4 wants a single authoritative "exactly what binary +
GGUF + quant + sha256 is loaded" payload so benchmarking and provenance
tooling can pin model identity across runs without re-parsing GGUF
headers in every consumer. The sidecar makes the SHA-256 free after the
first boot, which is what makes it usable as a default-on identity
field.

## Dependencies

None. This is purely additive on top of `gguf_inspect.{cpp,h}` as merged
in PR Luce-Org#305 — zero deletions, 333 insertions total. No other server
files or build rules change in this PR; consumers will be wired up
separately.

## Scope note

This PR is the extracted-and-cleaned remnant of the previously closed
PR Luce-Org#336 after a provenance audit; everything else from that branch
(c2_gate, qwen3 drafter changes, structural-defense loaders, and the
inadvertent reverts of Luce-Org#273/Luce-Org#295/Luce-Org#297) is either landing through its
canonical PR (Luce-Org#274) or being dropped entirely.
easel added a commit to easel/lucebox-hub that referenced this pull request Jun 8, 2026
## What

Adds a richer GGUF identity reader on top of Howard Su's existing
`gguf_inspect` (PR Luce-Org#305):

- `GgufMetadata` struct — captures `general.*` + `<arch>.*` header fields
  (architecture, name, file_type, quantization_version, block_count,
  embedding_length, context_length, vocab_size) with -1 / "" sentinels
  distinguishing "not in GGUF" from legitimate zero.
- `read_gguf_metadata(path, compute_sha256)` — best-effort header read;
  optional SHA-256 of the whole file.
- Self-contained SHA-256 mini-impl (RFC 6234) — no OpenSSL dependency
  added for one hash.
- `<path>.sha256` sidecar caching — first server start hashes the file
  (~30s for a 17 GB GGUF on NVMe), subsequent starts read the sidecar.
  Sidecar I/O failures are non-fatal.
- `llama_ftype_name` decode — maps `general.file_type` ints to
  human-readable names ("Q4_K_M", "IQ4_XS", etc.) for /props.

## Why

`/props` schema-4 wants a single authoritative "exactly what binary +
GGUF + quant + sha256 is loaded" payload so benchmarking and provenance
tooling can pin model identity across runs without re-parsing GGUF
headers in every consumer. The sidecar makes the SHA-256 free after the
first boot, which is what makes it usable as a default-on identity
field.

## Dependencies

None. This is purely additive on top of `gguf_inspect.{cpp,h}` as merged
in PR Luce-Org#305 — zero deletions, 333 insertions total. No other server
files or build rules change in this PR; consumers will be wired up
separately.

## Scope note

This PR is the extracted-and-cleaned remnant of the previously closed
PR Luce-Org#336 after a provenance audit; everything else from that branch
(c2_gate, qwen3 drafter changes, structural-defense loaders, and the
inadvertent reverts of Luce-Org#273/Luce-Org#295/Luce-Org#297) is either landing through its
canonical PR (Luce-Org#274) or being dropped entirely.
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