You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
laguna: support official Laguna-XS-2.1 GGUF metadata conventions
Poolside's official Laguna-XS-2.1-Q4_K_M.gguf uses different GGUF
metadata conventions than this fork's converter. Four fixes:
- vocab: accept tokenizer.ggml.pre = "laguna" as an alias for
llama-bpe (byte-identical vocab, same Llama-3 splitting regex)
- arch: new LLM_KV_ATTENTION_LAYER_TYPES key; laguna derives the
SWA mask from the attention.layer_types string array when the
sliding_window_pattern bool array is absent
- laguna: gate the shared-expert MLP on tensor presence instead of
expert_shared_count > 0 — XS-2.1 omits that KV, which silently
dropped the shared expert on every MoE layer (garbage output)
- chat: recognize the laguna_glm_thinking_v4 template marker (shipped
by XS-2.1 and M.1) in both the built-in template auto-detection and
the autoparser workaround; v4 is identical to v5 except for an
empty default system message
Verified: coherent greedy output, SWA 3:1 pattern + iswa cache in the
load log, clean reasoning split in llama-cli with no tag leaks;
test-llama-archs laguna and test-chat still pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-**Step 5 — Numerical validation**: Compare against HF Transformers. Requires CUDA/ROCm — not feasible on current hardware (AMD RX 7900 XT, no ROCm setup). Defer to a CUDA machine.
32
32
-**Step 7 — Quantization top-1 check**: Run f16 GGUF CPU-only (mmap) and compare top-1 tokens against Q4_K_M on GPU for a short prompt. Feasible but slow (f16 = 67 GB, exceeds 60 GB RAM; MoE means actual resident pages are manageable for a few tokens).
Poolside shipped an official `Laguna-XS-2.1-Q4_K_M.gguf` (in `/mnt/7dd.../LM Studio Models/poolside/Laguna-XS-2.1-GGUF/`). It is architecturally the same as XS.2 (same 40-layer 3:1 SWA layout, same per-layer head counts `[48,64,64,64,...]`, same MoE 256+1, same gate tensor sizes -> per-head, same n_embd=2048/head_dim=128) and the **tokenizer is byte-for-byte identical** (same 100352 tokens, 100026 merges, same token_type). But the GGUF metadata conventions differ from this fork's converter, which broke loading. Diagnosis + fixes:
37
+
38
+
**Fixed (model now loads):**
39
+
-`tokenizer.ggml.pre = "laguna"` (XS.2 wrote `"llama-bpe"`). llama.cpp rejected it with `unknown pre-tokenizer type: 'laguna'`. Since the vocab is identical to the working XS.2 (which gguf-py auto-detects as `llama-bpe` = `LLAMA_VOCAB_PRE_TYPE_LLAMA3`), `"laguna"` is just an alias. Added `"laguna"` to the LLAMA3 branch in `src/llama-vocab.cpp`.
40
+
- SWA pattern encoding differs: XS.2 writes `laguna.attention.sliding_window_pattern` (bool array); XS-2.1 writes `laguna.attention.layer_types` (string array: `"full_attention"`/`"sliding_attention"`) and omits the bool array. The laguna loader populated `is_swa_impl` only from the bool array, so XS-2.1 read every layer as global (wrong RoPE dims, no iswa cache). Added `LLM_KV_ATTENTION_LAYER_TYPES` (`%s.attention.layer_types`) to `llama-arch.h`/`.cpp`, and a fallback in `laguna.cpp::load_arch_hparams` that derives `is_swa_impl` from the strings when `sliding_window_pattern` is absent.
41
+
42
+
**Fixed (2026-07-05, second pass — model now fully working):**
43
+
-**Shared expert skipped (was the garbage root cause — confirmed).** XS-2.1 omits `laguna.expert_shared_count`, so `n_expert_shared` stayed 0 and the shared-expert MLP was gated off. Fix: gate on tensor presence (`if (model.layers[il].ffn_gate_shexp)`) instead of `n_expert_shared > 0` — matches the `wqkv_gate` pattern and is safe when tensors are absent. Output is now coherent ("The capital of France is" -> "…Paris." greedy).
44
+
-**SWA fix verified end-to-end**: load log shows `is_swa_any = 1`, correct 3:1 layer pattern (`is_swa` per-layer), `n_rot = 64` global / `n_rot_swa = 128`, and `llama_kv_cache_iswa` created with SWA layers filtered into the SWA cache.
45
+
-**XS-2.1 ships the `laguna_glm_thinking_v4` template marker** (same as M.1 — the two Open Items were one issue). The v4 template is functionally identical to v5 except the default system message is empty (v5 hardcodes the Poolside default). Added `laguna_glm_thinking_v4` to (a) the built-in template auto-detection in `src/llama-chat.cpp` (fixes `llama-completion` abort "this custom template is not supported") and (b) the Laguna autoparser workaround trigger in `common/chat-diff-analyzer.cpp` (without it, `llama-cli` classified the entire reply incl. `</think>`/`</assistant>` as reasoning).
46
+
-**Conversation mode works**: `llama-cli` peg crash is gone (it was downstream of the garbage); reasoning/content split cleanly, no `</think>`/`</assistant>` leaks.
47
+
- Benign: loader warns `special_eos_id/special_eot_id is not in special_eog_ids`, but both EOS (2) and EOT (24 `</assistant>`) do end up in the EOG set and generation stops correctly — cosmetic ordering quirk, no action.
48
+
- Note: `--no-conversation` is silently rejected by this `llama-cli` ("please use llama-completion instead"); `llama-completion` in turn asserts on `n_gpu_layers`, so raw-completion smoke tests need `llama-completion` left at its auto `-ngl` (do not pass `-ngl`).
49
+
- Regression: `test-llama-archs laguna` OK (all 3 backends), `test-chat` passes. `test-chat-auto-parser` has 206 pre-existing failures at baseline HEAD (incl. `poolside-Laguna.jinja`, 3/3) — unchanged by these fixes; not investigated.
50
+
51
+
**XS.2 vs XS-2.1 metadata diff (full):** XS.2-only keys: `attention.gate_per_head`, `attention.sliding_window_pattern`, `expert_shared_count`. XS-2.1-only keys: `attention.layer_types`, `mlp.layer_types`, `expert_gating_func` (=2, sigmoid), `vocab_size`, `quantize.imatrix.*`. Also `rope.scaling.factor` 64->32 and `original_context_length` 4096->8192 (YaRN base changed; data-driven, self-consistent, no code change needed). Of these, only `sliding_window_pattern` and `expert_shared_count` were load-bearing (both fixed).
52
+
34
53
### Key learnings from implementation
35
54
36
55
1.**`test-llama-archs` roundtrip test**: The test serializes and reloads models via `llama_model_saver`. If `llama_model_saver_supports_arch()` returns true for your arch, the roundtrip will run. The saver doesn't preserve all custom hparams (e.g., `sliding_window_pattern`), so either add the arch to the unsupported list or make those hparams optional with `false` in `get_key_or_arr`.
14.**Continuation with empty `reasoning.start` (assistant prefill)**: `generate_parser` in `chat-auto-parser-generator.cpp` only rebuilt the continuation generation prompt when `reasoning.start` was non-empty. With Laguna's delimiter-style workaround (`start=""`), content continuation appended the content directly after the prefilled `<think>` (inside the think block) and reasoning continuation dropped `reasoning_content` entirely. Fix: `else if (!reasoning.end.empty() && enable_thinking)` branch appends `msg.reasoning_content` (+ `reasoning.end` for CONTENT continuation) directly — the opening tag is already in the generation prompt. Continuation only reproduces the model's native format when the client round-trips reasoning/content verbatim (reasoning ends `\n`, content starts `\n`); hand-stripping those newlines makes the model re-emit a stray `</think>`.
49
68
15.**`reasoning_format=none` quirk (known, left as-is)**: with thinking on, content is the raw generated stream — `reasoning…</think>\nanswer` with **no opening `<think>`**, because the tag is prefilled in the prompt and never generated. Raw passthrough is consistent behavior; the peg parser cannot inject text that isn't in the stream.
50
69
16.**`layer_types` defaults to all full_attention (Laguna-M); rotary is data-driven**: `configuration_laguna.py` defaults `layer_types` to `["full_attention"]*n_layers` when absent (Laguna-M); Laguna-XS ships an explicit mix. The converter must mirror this or the per-layer `head_count`/`sliding_window_pattern` arrays come out empty. Partial rotary is now data-driven via `rope.dimension_count`/`rope.dimension_count_swa` (written from each rope config's `partial_rotary_factor`: XS.2 0.5 global / 1.0 SWA; M.1 1.0 everywhere), replacing the old hardcoded `n_rot_full /= 2`.
70
+
17.**`tokenizer.ggml.pre = "laguna"` is an alias for `llama-bpe`**: Poolside's official GGUFs label the BPE pre-tokenizer `"laguna"`; the fork's converter writes `"llama-bpe"`. Verified the vocab (tokens + merges + token_type) is byte-identical between the two, and gguf-py auto-detects this vocab as `llama-bpe` (= `LLAMA_VOCAB_PRE_TYPE_LLAMA3`), which produced correct XS.2 output. So the regex is the Llama-3 one; map `"laguna"` into the LLAMA3 branch in `src/llama-vocab.cpp`. The pre string only selects the splitting regex; it is not stored in the GGUF beyond the name.
71
+
18.**`is_swa_impl` must be populated from `attention.layer_types` strings when `sliding_window_pattern` is absent**: the framework's `is_swa(il)` reads only `is_swa_impl`, which laguna filled solely from the bool `sliding_window_pattern` array. Poolside's XS-2.1 GGUF ships string `attention.layer_types` (`"full_attention"`/`"sliding_attention"`) instead. Without the fallback, every layer reads as global -> wrong `n_rot` (64 vs 128) on SWA layers, no iswa cache, `swa_type=NONE`. Fix: `get_key_or_arr(SLIDING_WINDOW_PATTERN, ..., false)` returns `false` when absent; on that, read the `LLM_KV_ATTENTION_LAYER_TYPES` string array and set `is_swa_impl[il] = (layer_types[il] == "sliding_attention")`. Must run before the `swa_type = is_swa_any() ? ... : NONE` line.
72
+
19.**`n_expert_shared` defaults to 0; gate the shared expert on tensor presence, not the KV (FIXED)**: the laguna graph builder gated the shared-expert MLP on `if (hparams.n_expert_shared > 0)`. Poolside's XS-2.1 GGUF omits `laguna.expert_shared_count`, so `n_expert_shared` stayed 0 and the shared expert (tensors present and loaded via `TENSOR_NOT_REQUIRED`) was never applied -> garbage output. Fixed by gating on `if (model.layers[il].ffn_gate_shexp)` — presence-based, same pattern as `wqkv_gate`, and confirmed as the garbage root cause (coherent output immediately after).
73
+
20.**`laguna_glm_thinking_v4` vs `_v5` templates**: v4 (shipped by official XS-2.1 and M.1 GGUFs) is functionally identical to v5 (shipped by the fork's XS.2 conversion) except v4's default system message is empty while v5 hardcodes the Poolside default. Both markers must be recognized in TWO places: `src/llama-chat.cpp` auto-detection (else `common_chat_format_example` throws and llama-completion aborts at startup) and the `chat-diff-analyzer.cpp` Laguna workaround trigger (else the streaming parser misclassifies the whole reply as reasoning and leaks `</think>`/`</assistant>`).
51
74
52
75
---
53
76
@@ -141,6 +164,6 @@ Greedy decoding on `"The capital of France is"` → `" Paris.\nThe capital of Ge
141
164
142
165
## Open Items
143
166
-`moe_router_logit_softcapping` is absent from both Laguna-XS.2 and Laguna-M.1 `config.json` — no plumbing needed.
144
-
-**Laguna-M.1 chat template**: ships `laguna_glm_thinking_v4`, but auto-detection in `src/llama-chat.cpp` keys on the `laguna_glm_thinking_v5` marker, so the built-in matcher rejects it (`custom template not supported`). Workaround: pass `--jinja`. Follow-up: add the v4 marker to auto-detection.
167
+
-~~**Laguna-M.1 chat template**: ships `laguna_glm_thinking_v4`, rejected by the v5-only auto-detection.~~**Resolved 2026-07-05** as part of XS-2.1 support: the v4 marker is now recognized in both `src/llama-chat.cpp` auto-detection and the `chat-diff-analyzer.cpp` workaround (learning 20).
145
168
-**Step 5 / Step 7** still deferred — numerical validation needs CUDA; the 226B M.1 is also impractical for the quant top-1 check on current hardware.
146
169
-**Pre-existing XS.2 GGUFs need reconversion** — they predate the now-required `rope.dimension_count` and would otherwise get full rotary instead of half. **In progress (2026-06-20):** re-cloning latest `poolside/Laguna-XS.2` and reconverting to f16 → Q4_K_M/IQ4_XS; also fixed a converter `gate_per_head` bug (learning 12) that would otherwise have made the new GGUF fail to load.
0 commit comments