Skip to content

Support poolside/Laguna-S-2.1-NVFP4, and fix serving from source on Windows - #298

Open
Eng-Ahmd wants to merge 2 commits into
FlashML-org:mainfrom
Eng-Ahmd:feat/laguna-s-2.1-nvfp4
Open

Support poolside/Laguna-S-2.1-NVFP4, and fix serving from source on Windows#298
Eng-Ahmd wants to merge 2 commits into
FlashML-org:mainfrom
Eng-Ahmd:feat/laguna-s-2.1-nvfp4

Conversation

@Eng-Ahmd

Copy link
Copy Markdown

Adds support for poolside/Laguna-S-2.1-NVFP4, and fixes the Windows issues that prevented serving from a source checkout.

Laguna S 2.1

A 48-layer, 117.6B/8.5B-active MoE. Five traits sit outside what the existing model modules cover:

  • Per-layer attention width. 48 heads on the 12 full-attention layers, 72 on the 36 sliding ones (window 512); KV stays at 8 heads throughout, so the GQA ratio differs per layer.
  • Two RoPE configs, nested under rope_parameters by layer type: YaRN (theta 500k, partial_rotary_factor 0.5) for full attention, default (theta 10k, full rotary) for sliding. There is no top-level rope_theta.
  • Softplus output gating applied per head before o_proj, and unconditional QK-RMSNorm with no config flag.
  • Sigmoid router with a selection-only correction bias, a routed scaling factor of 2.5, and one always-on shared expert. Layer 0 is dense.
  • Mixed-precision experts. Layers 1-39 ship compressed-tensors NVFP4 (group 16); layers 40-47 ship plain bf16.

Two details worth calling out for review:

ModelConfig.num_qo_heads is set to the maximum per-layer head count, not the nominal one. The Triton backend sizes its decode scratch and CUDA-graph capture buffers from that single value and never resizes them on replay, so the nominal 48 would let the 72-head layers write past their buffer. Per-layer counts drive the projections.

The bf16 expert layers are quantized to NVFP4 during conversion so one bank layout and one kernel path serves all 47 MoE layers. To keep that on the shared loader rather than a private one, Nvfp4ExpertSourceSpec gains three optional hooks: kind_map (compressed-tensors key names), global_transform (its quant-side scale convention), and synthesize_layer (bank layers absent from the checkpoint). Existing specs leave all three None and are unchanged.

Verification

Checked against the checkpoint and its reference modeling_laguna.py:

Check Result
Converted banks, packed layers dequantize bit-exactly vs source
Converted banks, synthesized layers 9.51% rel. RMS vs a 9.41% floor measured with ideal fp32 scales
RoPE, both layer types ~5e-07 vs reference
Router expert ids identical; weights match after the reference's bf16 cast
Dense tensors incl. fused gate/up exactly equal
Loader key set vs model state dict 625/625, no missing or extra

Then end to end: coherent generation, thinking and tool calls parsed, batched CUDA-graph replay, generation past the sliding window, and a runtime cache rebuild followed by further generation.

Reasoning and tool parsers resolve automatically. Laguna's think tags are <think>/</think> with the opening tag pre-filled by the chat template, so the model emits only the closing tag; its tool envelope is GLM-4.7's grammar without the inter-tag newlines, which Glm47Detector already tolerates.

Windows

Five POSIX-only assumptions each stop python -m freetoken before it can answer a request. None change POSIX behaviour.

  • ZMQ IPC. libzmq has no ipc:// transport on Windows, so all five inter-process sockets fail to bind. Addresses now come from one helper that keeps ipc:// where the build supports it and otherwise uses a contiguous localhost TCP block, reserved once in the parent and inherited by the workers through the config.
  • Event loop. pyzmq's asyncio sockets need add_reader, absent from the Proactor loop. The frontend's ZMQ listener died at startup and requests hung until timeout while the scheduler logged the prefill normally. Setting the loop policy is not sufficient — uvicorn's loop factory hardcodes ProactorEventLoop on win32 — so the server runs on a selector loop passed in via loop="none".
  • os.posix_fadvise, mmap.PROT_READ, mmap.madvise are unavailable. The first raised AttributeError past the OSError guard; the second is access=ACCESS_READ on Windows. The parallel O_DIRECT reader was already platform-gated, so page-cache dropping becomes a no-op and host memory pressure during a large conversion is higher.

Verified by converting and serving the 93 GiB checkpoint from a source checkout on Windows 11: prompts answer in ~2s, and a cache rebuild followed by further generation works.

Notes for reviewers

  • kernel/aot_models.py gains a Laguna entry; both KV groups share 8x128 geometry, so one store variant covers them (same as gpt-oss). Experts keep the native 6-bank nvfp4 layout because I=1024 resolves select_nvfp4_backend to the Triton kernels.
  • Tested on an RTX 5080 Laptop (sm_120, 16 GB) with the offload MoE backend and split CPU/GPU bank residency.
  • Tensor parallelism is asserted off: Laguna keeps q/k/v/g/o split and its per-layer head counts are not sharded by this module.

Laguna S 2.1 is a 48-layer, 117.6B/8.5B-active MoE with several traits no
existing model module covers:

* Per-layer attention width: 48 heads on the 12 full-attention layers, 72 on
  the 36 sliding ones (window 512), with KV fixed at 8 heads.
* Two RoPE configs nested under `rope_parameters` by layer type -- YaRN
  (theta 500k, partial_rotary_factor 0.5) for full, default (theta 10k, full
  rotary) for sliding. There is no top-level `rope_theta`.
* Softplus output gating applied per head before o_proj, and unconditional
  QK-RMSNorm with no config flag.
* Sigmoid router with a selection-only correction bias, `norm_topk_prob`, a
  routed scaling factor of 2.5, and one always-on shared expert. Layer 0 is
  dense.
* Mixed-precision experts: layers 1-39 ship compressed-tensors NVFP4
  (`weight_packed` / `weight_scale` / `weight_global_scale`, group 16), while
  40-47 ship plain bf16.

`ModelConfig.num_qo_heads` is set to the maximum per-layer head count rather
than the nominal one: the Triton backend sizes its decode scratch and
CUDA-graph capture buffers from that single value and never resizes them on
replay, so the nominal 48 would let the 72-head layers write out of bounds
under graph replay.

The bf16 expert layers are quantized to NVFP4 at conversion so one bank layout
and one kernel path covers all 47 MoE layers. `Nvfp4ExpertSourceSpec` gains
three optional hooks to keep this on the shared loader (with its completion
tracking, pin-after-fill and FTW streaming) instead of a private one:
`kind_map` for compressed-tensors key names, `global_transform` for its
quant-side scale convention, and `synthesize_layer` for bank layers absent from
the checkpoint. Existing specs leave all three None and are unaffected.

Verified against the checkpoint and its reference implementation: converted
banks dequantize bit-exactly for the packed layers; the synthesized layers land
at 9.51% relative RMS against a 9.41% floor measured with ideal fp32 scales;
RoPE matches `modeling_laguna.py` to ~5e-07 for both layer types; router expert
ids are identical and weights match once the reference's bf16 cast is applied.

Reasoning and tool-call parsers resolve automatically: Laguna's think tags are
`<think>`/`</think>` with the opening tag pre-filled by the chat template, and
its tool envelope is GLM-4.7's grammar without the inter-tag newlines.
Five separate POSIX-only assumptions each stop `python -m freetoken` before it
can answer a request on Windows. None affect POSIX behaviour.

* ZMQ IPC. libzmq is built without the `ipc://` transport on Windows, so all
  five inter-process sockets fail to bind with "Protocol not supported". The
  addresses now come from one helper on SchedulerConfig that keeps `ipc://`
  wherever the ZMQ build supports it and otherwise uses a contiguous localhost
  TCP block. The base port is reserved once in the parent and travels to the
  workers in the config they are already handed, since separate processes cannot
  negotiate it at runtime.

* Event loop. pyzmq's asyncio sockets need `add_reader`, which the Windows
  Proactor loop does not implement. The frontend's ZMQ listener died at startup
  and every request then hung until it timed out while the scheduler logged the
  prefill as normal. Setting the loop policy alone is not enough because
  uvicorn's loop factory hardcodes ProactorEventLoop on win32, so the server now
  runs on a selector loop we create and pass in via `loop="none"`.

* `os.posix_fadvise`, `mmap.PROT_READ` and `mmap.madvise` are absent on Windows;
  the first raised AttributeError past the OSError guard, and the second is
  spelled `access=ACCESS_READ` there. The parallel O_DIRECT reader was already
  gated behind a platform check, so page-cache dropping simply becomes a no-op
  and host memory pressure during a large conversion is correspondingly higher.

Checked by converting and serving a 93 GiB NVFP4 checkpoint from a source
checkout: prompts answer in ~2s, thinking and tool calls parse, and a runtime
cache rebuild followed by further generation works.
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.

1 participant