README.md CHANGED
@@ -33,7 +33,7 @@ Laguna XS 2.1-FP8 is a 33B total parameter Mixture-of-Experts model with 3B acti
33
  - **Mixed SWA and global attention layout**: Laguna XS 2.1 uses sigmoid gating with per-layer rotary scales, enabling mixed SWA (Sliding Window Attention) and global attention layers in a 3:1 ratio (across 40 total layers)
34
  - **KV cache in FP8**: KV cache quantized to FP8, reducing memory per token
35
  - **Native reasoning support**: Interleaved thinking between tool calls with support for enabling and disabling thinking per-request
36
- - **Local-ready**: At 33B total parameters and 3B activated, Laguna XS 2.1 is compact enough to run on a Mac with 36 GB of RAM. Available on [Ollama](https://ollama.com/library/laguna-xs-2.1) and [llama.cpp](https://github.com/ggml-org/llama.cpp/pull/25165) (BF16 and Q4\_K\_M only)
37
  - **OpenMDW-1.1 license**: Use and modify the model and associated materials freely for commercial and non-commercial purposes ([learn more about OpenMDW](https://openmdw.ai/))
38
 
39
  ---
@@ -107,9 +107,7 @@ Laguna XS 2.1-FP8 is supported in vLLM, SGLang and Transformers, and TRT-LLM tha
107
 
108
  #### vLLM
109
 
110
- The full vLLM recipe is on the main [Laguna XS 2.1 model card](https://huggingface.co/poolside/Laguna-XS-2.1) and on the [vLLM recipes page](https://recipes.vllm.ai/poolside/Laguna-XS-2.1). Quantization is detected automatically from `quantization_config` in this checkpoint, so the same command works with `poolside/Laguna-XS-2.1-FP8` substituted for the model ID. Set `VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER=0` when serving with vLLM.
111
- > [!IMPORTANT]
112
- > Tool calling requires a vLLM build with [vllm-project/vllm#47311](https://github.com/vllm-project/vllm/pull/47311); on older builds pass `--tool-call-parser glm47` (the GLM 4.7 parser) instead of `poolside_v1`.
113
 
114
  > [!NOTE]
115
  > The FP8-quantized KV cache requires vLLM >= 0.22.0. Earlier versions produce scrambled output on non-Hopper GPUs because of a per-layer attention-head count bug, fixed in [vllm#42650](https://github.com/vllm-project/vllm/pull/42650). On older vLLM, disable the FP8 KV cache by adding `--kv-cache-dtype-skip-layers $(seq 0 39)`.
@@ -134,10 +132,13 @@ llm = LLM(model="poolside/Laguna-XS-2.1-FP8", trust_remote_code=True)
134
 
135
  #### Ollama
136
 
137
- Available on the [Ollama library](https://ollama.com/library/laguna-xs-2.1).
138
 
139
- > [!NOTE]
140
- > **macOS (Metal) users:** Chat (`ollama run` / `/api/chat`) works as expected on Linux/CUDA. On macOS/Metal it may currently return empty output; the root cause is not yet fully understood and we're investigating it with the Ollama team. On a Mac, use a Linux/CUDA host, or the `/api/generate` endpoint with `"raw": true`.
 
 
 
141
 
142
  ## Controlling reasoning
143
 
 
33
  - **Mixed SWA and global attention layout**: Laguna XS 2.1 uses sigmoid gating with per-layer rotary scales, enabling mixed SWA (Sliding Window Attention) and global attention layers in a 3:1 ratio (across 40 total layers)
34
  - **KV cache in FP8**: KV cache quantized to FP8, reducing memory per token
35
  - **Native reasoning support**: Interleaved thinking between tool calls with support for enabling and disabling thinking per-request
36
+ - **Local-ready**: At 33B total parameters and 3B activated, Laguna XS 2.1 is compact enough to run on a Mac with 36 GB of RAM. Available on [Ollama](https://ollama.com/library/laguna-xs.2.1) and [llama.cpp](https://github.com/ggml-org/llama.cpp/pull/25165) (BF16 and Q4\_K\_M only)
37
  - **OpenMDW-1.1 license**: Use and modify the model and associated materials freely for commercial and non-commercial purposes ([learn more about OpenMDW](https://openmdw.ai/))
38
 
39
  ---
 
107
 
108
  #### vLLM
109
 
110
+ The full vLLM recipe is on the main [Laguna XS 2.1 model card](https://huggingface.co/poolside/Laguna-XS-2.1) and on the [vLLM recipes page](https://recipes.vllm.ai/poolside/Laguna-XS-2.1). Quantization is detected automatically from `quantization_config` in this checkpoint, so the same command works with `poolside/Laguna-XS-2.1-FP8` substituted for the model ID. No extra flags required.
 
 
111
 
112
  > [!NOTE]
113
  > The FP8-quantized KV cache requires vLLM >= 0.22.0. Earlier versions produce scrambled output on non-Hopper GPUs because of a per-layer attention-head count bug, fixed in [vllm#42650](https://github.com/vllm-project/vllm/pull/42650). On older vLLM, disable the FP8 KV cache by adding `--kv-cache-dtype-skip-layers $(seq 0 39)`.
 
132
 
133
  #### Ollama
134
 
135
+ Visit [Ollama's model library](https://ollama.com/library/laguna-xs.2.1) to pull to your local machine.
136
 
137
+ > **macOS (Metal) note:** Chat (`ollama run` / `/api/chat`) works on Linux/CUDA —
138
+ > coherent output with reasoning and tool-calling intact. On macOS/Metal it may
139
+ > currently return empty output; the root cause is not yet fully understood and
140
+ > we're investigating it with the Ollama team. On a Mac, use a Linux/CUDA host, or
141
+ > the `/api/generate` endpoint with `"raw": true`.
142
 
143
  ## Controlling reasoning
144
 
chat_template.jinja CHANGED
@@ -1,93 +1,132 @@
1
- {#- Iteration on laguna_glm_thinking_v8/chat_template.jinja -#}
2
- {#- No formatting instructions -#}
3
  {{- "〈|EOS|〉" -}}
4
  {%- set enable_thinking = enable_thinking | default(false) -%}
 
5
  {%- set add_generation_prompt = add_generation_prompt | default(false) -%}
6
 
7
  {#- ───── header (system message) ───── -#}
8
- {#- A caller-supplied system message with empty content opts out of the default below, producing no <system> block — used to train without a system message. -#}
9
- {%- set system_message = "You are a helpful, conversationally-fluent assistant made by Poolside. You are here to be helpful to users through natural language conversations." -%}
10
  {%- if messages and messages[0].role == "system" -%}
11
  {%- set system_message = messages[0].content -%}
12
- {%- set messages = messages[1:] -%}
13
  {%- endif -%}
14
 
15
- {%- set has_sys = system_message and system_message.strip() -%}
16
- {%- if has_sys or tools or enable_thinking -%}
17
- {{- "<system>" -}}
18
 
19
- {%- if has_sys -%}
 
20
  {{- system_message.rstrip() -}}
21
- {%- if tools -%}{{- "\n\n" -}}{%- endif -%}
22
  {%- endif -%}
23
 
24
  {%- if tools -%}
25
- {{- "### Tools\n\n" -}}
26
- {{- "You may call functions to assist with the user query.\n" -}}
27
- {{- "All available function signatures are listed below:\n" -}}
28
- {{- "<available_tools>\n" -}}
29
  {%- for tool in tools -%}
30
- {{- (tool | tojson) ~ "\n" -}}
31
  {%- endfor -%}
32
- {{- "</available_tools>" -}}
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  {%- endif -%}
34
 
35
- {{- "</system>\n" -}}
36
  {%- endif -%}
37
 
38
  {#- ───── main loop ───── -#}
39
  {%- for message in messages -%}
40
  {%- set content = message.content if message.content is string else "" -%}
41
  {%- if message.role == "user" -%}
42
- {{- "<user>" + content + "</user>\n" -}}
43
  {%- elif message.role == "assistant" -%}
44
  {%- generation -%}
45
- {{- "<assistant>" -}}
46
- {#- Extract reasoning content from message.reasoning (vLLM field name) or message.reasoning_content -#}
47
- {%- set reasoning_content = '' -%}
48
- {%- if message.reasoning is string -%}
49
- {%- set reasoning_content = message.reasoning -%}
50
- {%- elif message.reasoning_content is string -%}
51
- {%- set reasoning_content = message.reasoning_content -%}
52
- {%- endif -%}
53
- {#- Display reasoning content for all messages if enable_thinking -#}
54
- {%- if enable_thinking -%}
55
- {{- '<think>' + reasoning_content + '</think>' -}}
56
- {%- else -%}
57
- {{- '</think>' -}}
58
- {%- endif -%}
59
- {#- Display main content (trailing newline only when no tool_calls follow) -#}
60
- {%- if content -%}
61
  {{- content -}}
62
- {%- endif -%}
63
- {%- if message.tool_calls -%}
64
- {%- for tool_call in message.tool_calls -%}
65
- {%- set function_data = tool_call.function -%}
66
- {{- '<tool_call>' + function_data.name -}}
67
- {%- set _args = function_data.arguments -%}
68
- {%- for k, v in _args.items() -%}
69
- {{- "<arg_key>" ~ k ~ "</arg_key>" -}}
70
- {{- "<arg_value>" -}}{{- v | tojson(ensure_ascii=False) if v is not string else v -}}{{- "</arg_value>" -}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  {%- endfor -%}
72
- {{- "</tool_call>" -}}
73
- {%- endfor -%}
74
  {%- endif -%}
75
- {{- "</assistant>\n" -}}
76
  {%- endgeneration -%}
77
  {%- elif message.role == "tool" -%}
78
- {{- "<tool_response>" + content + "</tool_response>\n" -}}
79
- {%- elif message.role == "system" -%}
80
- {#- Render additional system messages (the first one, if any, is handled separately in the header and was sliced off above) -#}
81
- {{- "<system>" + content + "</system>\n" -}}
82
  {%- endif -%}
83
  {%- endfor -%}
84
  {#- ───── generation prompt ───── -#}
85
  {%- if add_generation_prompt -%}
86
- {{- "<assistant>" -}}
87
  {#- ───── Include reasoning mode directive ───── -#}
88
- {%- if enable_thinking -%}
89
- {{- '<think>' -}}
90
- {%- else -%}
91
  {{- '</think>' -}}
92
- {%- endif -%}
93
- {%- endif -%}
 
 
 
1
+ {#- Copied from laguna_glm_thinking_v4/chat_template.jinja -#}
2
+ {#- Removes prefix that references <think> token, and replaces message.reasoning_content reference with message.reasoning -#}
3
  {{- "〈|EOS|〉" -}}
4
  {%- set enable_thinking = enable_thinking | default(false) -%}
5
+ {%- set render_assistant_messages_raw = render_assistant_messages_raw | default(false) -%}
6
  {%- set add_generation_prompt = add_generation_prompt | default(false) -%}
7
 
8
  {#- ───── header (system message) ───── -#}
9
+ {%- set system_message = "" -%}
 
10
  {%- if messages and messages[0].role == "system" -%}
11
  {%- set system_message = messages[0].content -%}
 
12
  {%- endif -%}
13
 
14
+ {%- if (system_message and system_message.strip()) or tools -%}
15
+ {{- "<system>\n" -}}
 
16
 
17
+ {%- if system_message and system_message.strip() -%}
18
+ {{- "\n" -}}
19
  {{- system_message.rstrip() -}}
 
20
  {%- endif -%}
21
 
22
  {%- if tools -%}
23
+ {{- "\n\n### Tools\n\n" -}}
24
+ {%- set ns = namespace(tool_string="You may call functions to assist with the user query.\n"
25
+ ~ "All available function signatures are listed below:\n"
26
+ ~ "<available_tools>\n") -%}
27
  {%- for tool in tools -%}
28
+ {%- set ns.tool_string = ns.tool_string ~ (tool | tojson) ~ "\n" -%}
29
  {%- endfor -%}
30
+ {%- if enable_thinking -%}
31
+ {%- set tool_string = ns.tool_string + "</available_tools>\n\n" ~
32
+ "Wrap your thinking in '<think>', '</think>' tags, followed by a function call. For each function call, return an unescaped XML-like object with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n" ~
33
+ "<think> your thoughts here </think>\n" ~
34
+ "<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n" ~
35
+ "</tool_call>" -%}
36
+ {%- else -%}
37
+ {%- set tool_string = ns.tool_string + "</available_tools>\n\n" ~
38
+ "For each function call, return an unescaped XML-like object " ~
39
+ "with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n" ~
40
+ "<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n" ~
41
+ "</tool_call>" -%}
42
+ {%- endif -%}
43
+ {{- tool_string -}}
44
  {%- endif -%}
45
 
46
+ {{- "\n</system>\n" -}}
47
  {%- endif -%}
48
 
49
  {#- ───── main loop ───── -#}
50
  {%- for message in messages -%}
51
  {%- set content = message.content if message.content is string else "" -%}
52
  {%- if message.role == "user" -%}
53
+ {{- "<user>\n" + content + "\n</user>\n" -}}
54
  {%- elif message.role == "assistant" -%}
55
  {%- generation -%}
56
+ {{- "<assistant>\n" -}}
57
+ {%- if render_assistant_messages_raw -%}
58
+ {#- Raw mode: prepend the generation prompt token, then dump content verbatim. -#}
59
+ {#- The generation prompt is <think> when enable_thinking, </think> otherwise. -#}
60
+ {#- Only prepend if content doesn't already start with it. -#}
61
+ {%- if enable_thinking -%}
62
+ {%- if not content.startswith('<think>') -%}
63
+ {{- '<think>' -}}
64
+ {%- endif -%}
65
+ {%- else -%}
66
+ {%- if not content.startswith('</think>') -%}
67
+ {{- '</think>' -}}
68
+ {%- endif -%}
69
+ {%- endif -%}
 
 
70
  {{- content -}}
71
+ {#- Append closing tag if content doesn't already end with it. -#}
72
+ {%- if not content.endswith('</assistant>\n') and not content.endswith('</assistant>') -%}
73
+ {{- '\n</assistant>' -}}
74
+ {%- endif -%}
75
+ {{- "\n" -}}
76
+ {%- else -%}
77
+ {#- Extract reasoning content from message.reasoning (vLLM field name) or message.reasoning_content, or from <think> tags -#}
78
+ {%- set reasoning_content = '' %}
79
+ {%- if message.reasoning is string %}
80
+ {%- set reasoning_content = message.reasoning %}
81
+ {%- elif message.reasoning_content is string %}
82
+ {%- set reasoning_content = message.reasoning_content %}
83
+ {%- endif %}
84
+ {#- Always strip <think> tags from content if present to avoid duplication -#}
85
+ {%- if '</think>' in content %}
86
+ {%- if not reasoning_content %}
87
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
88
+ {%- endif %}
89
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
90
+ {%- endif %}
91
+ {#- Display reasoning content for all messages -#}
92
+ {%- if reasoning_content -%}
93
+ {{- '<think>\n' + reasoning_content.strip() + '\n</think>\n' -}}
94
+ {%- else -%}
95
+ {{- '</think>\n' -}}
96
+ {%- endif -%}
97
+ {#- Display main content -#}
98
+ {%- if content.strip() -%}
99
+ {{- content.strip() ~ "\n" -}}
100
+ {%- endif -%}
101
+ {%- if message.tool_calls -%}
102
+ {%- for tool_call in message.tool_calls -%}
103
+ {%- set function_data = tool_call.function -%}
104
+ {{- '<tool_call>' + function_data.name }}
105
+ {% set _args = function_data.arguments %}
106
+ {%- for k, v in _args.items() -%}
107
+ {{- "<arg_key>" ~ k ~ "</arg_key>\n" -}}
108
+ {{- "<arg_value>"}}{{ v | tojson(ensure_ascii=False) if v is not string else v }}{{ "</arg_value>\n" -}}
109
+ {%- endfor -%}
110
+ {{- "</tool_call>\n" -}}
111
  {%- endfor -%}
112
+ {%- endif -%}
113
+ {{- "</assistant>\n" -}}
114
  {%- endif -%}
 
115
  {%- endgeneration -%}
116
  {%- elif message.role == "tool" -%}
117
+ {{- "<tool_response>\n" + content + "\n</tool_response>\n" -}}
118
+ {%- elif message.role == "system" and loop.index0 != 0 -%}
119
+ {#- Render additional system messages (skip the first one which is handled separately in the header) -#}
120
+ {{- "<system>\n" + content + "\n</system>\n" -}}
121
  {%- endif -%}
122
  {%- endfor -%}
123
  {#- ───── generation prompt ───── -#}
124
  {%- if add_generation_prompt -%}
125
+ {{- "<assistant>\n" -}}
126
  {#- ───── Include reasoning mode directive ───── -#}
127
+ {%- if not enable_thinking %}
 
 
128
  {{- '</think>' -}}
129
+ {%- else %}
130
+ {{- '<think>' -}}
131
+ {%- endif %}
132
+ {%- endif -%}
config.json CHANGED
@@ -47,7 +47,7 @@
47
  "original_max_position_embeddings": 8192,
48
  "beta_slow": 1.0,
49
  "beta_fast": 64.0,
50
- "attention_factor": 1.3465735902799727,
51
  "partial_rotary_factor": 0.5
52
  },
53
  "sliding_attention": {
@@ -335,4 +335,4 @@
335
  },
336
  "version": "0.14.1.dev11+gf2ee47b"
337
  }
338
- }
 
47
  "original_max_position_embeddings": 8192,
48
  "beta_slow": 1.0,
49
  "beta_fast": 64.0,
50
+ "attention_factor": 1.0,
51
  "partial_rotary_factor": 0.5
52
  },
53
  "sliding_attention": {
 
335
  },
336
  "version": "0.14.1.dev11+gf2ee47b"
337
  }
338
+ }
configuration_laguna.py CHANGED
@@ -186,12 +186,6 @@ class LagunaConfig(PreTrainedConfig):
186
  if rope_parameters is None:
187
  rope_parameters = {"rope_type": "default", "rope_theta": 500000.0}
188
 
189
- # config.json stores SWA rope nested in rope_parameters["sliding_attention"]
190
- # and carries no top-level swa_rope_parameters. Derive it here, else the
191
- # sliding-window layers silently reuse the full-attention rope.
192
- if swa_rope_parameters is None and isinstance(rope_parameters, dict):
193
- swa_rope_parameters = rope_parameters.get("sliding_attention")
194
-
195
  # If ``partial_rotary_factor`` is set at the top level, inject it into any
196
  # rope dict that does not already carry one so the rotary embedding picks
197
  # it up consistently for both full-attention and SWA layers.
 
186
  if rope_parameters is None:
187
  rope_parameters = {"rope_type": "default", "rope_theta": 500000.0}
188
 
 
 
 
 
 
 
189
  # If ``partial_rotary_factor`` is set at the top level, inject it into any
190
  # rope dict that does not already carry one so the rotary embedding picks
191
  # it up consistently for both full-attention and SWA layers.
generation_config.json CHANGED
@@ -10,7 +10,6 @@
10
  "temperature": 1.0,
11
  "top_p": 1.0,
12
  "min_p": 0.0,
13
- "top_k": 20,
14
  "speculative_config": {
15
  "method": "dflash",
16
  "source": "huggingface",
 
10
  "temperature": 1.0,
11
  "top_p": 1.0,
12
  "min_p": 0.0,
 
13
  "speculative_config": {
14
  "method": "dflash",
15
  "source": "huggingface",
modeling_laguna.py CHANGED
@@ -633,8 +633,8 @@ class LagunaModel(LagunaPreTrainedModel):
633
  use_cache: bool | None = None,
634
  **kwargs: Unpack[TransformersKwargs],
635
  ) -> MoeModelOutputWithPast:
636
- from transformers.cache_utils import DynamicCache
637
- from transformers.masking_utils import create_causal_mask, create_sliding_window_causal_mask
638
 
639
  if (input_ids is None) ^ (inputs_embeds is not None):
640
  raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
@@ -877,20 +877,3 @@ class LagunaForCausalLM(LagunaPreTrainedModel, GenerationMixin):
877
 
878
 
879
  __all__ = ["LagunaForCausalLM", "LagunaModel", "LagunaPreTrainedModel"]
880
-
881
-
882
- # --- Added: register the native Laguna checkpoint-conversion for trust_remote_code loads.
883
- # transformers >=5.12 skips checkpoint-conversion mappings for custom (remote) code
884
- # unless explicitly registered, which broke loading the shipped per-expert MoE weights.
885
- try:
886
- from transformers.conversion_mapping import (
887
- get_checkpoint_conversion_mapping as _lg_get,
888
- register_checkpoint_conversion_mapping as _lg_reg,
889
- USER_REGISTERED_MAPPINGS as _lg_user,
890
- )
891
- if "laguna" not in _lg_user:
892
- _lg_m = _lg_get("laguna")
893
- if _lg_m is not None:
894
- _lg_reg("laguna", _lg_m, overwrite=True)
895
- except Exception:
896
- pass
 
633
  use_cache: bool | None = None,
634
  **kwargs: Unpack[TransformersKwargs],
635
  ) -> MoeModelOutputWithPast:
636
+ from ...cache_utils import DynamicCache
637
+ from ...masking_utils import create_causal_mask, create_sliding_window_causal_mask
638
 
639
  if (input_ids is None) ^ (inputs_embeds is not None):
640
  raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
 
877
 
878
 
879
  __all__ = ["LagunaForCausalLM", "LagunaModel", "LagunaPreTrainedModel"]