| Property | Value |
|---|---|
| Provider | OpenAI |
| GGUF architecture keys | gptoss, gpt-oss |
| Source class | GptOssModel (legacy per-seq) + GptOssModel.BatchedForward.cs (IBatchedPagedModel) |
| Example models | gpt-oss-20b |
| Modalities | Text only |
| Thinking mode | Yes (Harmony format: <|channel>analysis ... <|channel>final) |
| Tool calling | Yes (Harmony commentary channel — to=functions.NAME) |
| Batched / paged forward | Default ON — set TS_GPTOSS_BATCHED=0 to force the legacy per-sequence KV-swap path for A/B comparison. Per-layer paged K/V plus attention sinks via native TSGgml_PagedAttentionForwardWithSinks (or managed C# fallback via TS_GPTOSS_PAGED_ATTN_MANAGED=1). See §11. |
| Output parser | HarmonyOutputParser (always required) |
Verified GGUF pointer:
| Model | HF repo | Recommended file | Notes |
|---|---|---|---|
| gpt-oss-20b (MoE) | ggml-org/gpt-oss-20b-GGUF | gpt-oss-20b-mxfp4.gguf (12.110 GB) |
Native MXFP4 expert quantization; text only. Thinking is always on (Harmony analysis channel) and tool calling is supported. Official upstream: openai/gpt-oss-20b (Apache-2.0). |
Command-line download (one line per file; requires pip install -U huggingface_hub):
python -m pip install -U huggingface_hub
hf download ggml-org/gpt-oss-20b-GGUF gpt-oss-20b-mxfp4.gguf --local-dir modelsCLI one-shot (the text prompt comes from a file via --input; CLI sampling
defaults to greedy and --max-tokens defaults to 100):
dotnet run --project TensorSharp.Cli -c Release -- --model models/gpt-oss-20b-mxfp4.gguf \
--input prompt.txt --max-tokens 512 --backend ggml_cudaServer (Web UI + OpenAI/Ollama-compatible APIs on http://localhost:5000):
dotnet run --project TensorSharp.Server -c Release -- --model models/gpt-oss-20b-mxfp4.gguf \
--backend ggml_cuda --max-tokens 4096GPT OSS is OpenAI's open-weights MoE family. Several design choices set it apart from the other architectures in TensorSharp:
- MoE in every block, with TopK routing followed by a softmax over the selected experts only (in contrast to Gemma 4, which softmax-then-TopKs).
- Attention sinks — a learned per-head bias that participates in the softmax as a "virtual token" with zero K and zero V, providing an always-available attention target.
- Bias on every linear projection (Q, K, V, attn output, gate, up, down, router). This is unique among the architectures TensorSharp supports.
- Clamped GLU activation — the FFN uses a SiLU-like activation
x · sigmoid(α · x) · (y + 1)with the gate clamped to[-∞, 7]and theupvalue clamped to[-7, 7]. - Harmony output format — every output is wrapped in
<|channel>analysis ... <|channel>final ...tags. The output parser is markedAlwaysRequired = truebecause there is no "no thinking" mode. - MXFP4 expert weights (GGML quantization type 39) — a 4-bit microscaling format used by the gpt-oss-20b release.
tokens (int[])
│
token_embd.weight
│
┌──── × NumLayers ───────────────────────────────┐
│ RMSNorm(attn_norm) │
│ Q+bias, K+bias, V+bias (no QK-norm) │
│ RoPE_NeoX with YaRN scaling │
│ attention with sinks │
│ attn_output + bias ─► residual │
│ │
│ RMSNorm(post_attention_norm) │
│ router_linear+bias │
│ topK then softmax(selected experts) │
│ per-expert: SiLUAlphaLimit(gate+bias, up+bias) │
│ ─► down+bias │
│ weighted sum of expert outputs │
│ ─► residual │
└──────────────────────────────────────────────────┘
│
RMSNorm(output_norm)
│
LM head (output.weight)
│
▼
logits
For each layer L:
hidden ─► RMSNorm(attn_norm.weight, eps)
─► Q+bias, K+bias, V+bias (each linear is a matmul + per-row bias add)
─► RoPE_NeoX with Yarn scaling
• origCtxLen = Config.OriginalContextLength (4096)
• freqScale = 1 / RopeScale
• beta_fast = 32, beta_slow = 1
─► append (K, V) to KV cache
─► attention with sinks:
scores = Q × K^T
scores ← apply causal mask (and SWA on even layers when desired)
sinks = attn_sinks.weight # [numHeads]
sums = exp(scores - max) + exp(sinks - max)
scores = exp(scores - max) / sums # softmax including sink
out = scores × V
─► attn_output.weight matmul + bias → o
─► residual = hidden + o
moeInput = (last layer in prefill) ? narrow(seq_len-1) : residual
moeInput ─► RMSNorm(post_attention_norm)
─► router_linear + bias
─► topK(_numExpertsUsed)
─► softmax(selected weights)
─► For each selected expert e:
gate = ffn_gate_up_exps[e][:, :nFf] × moeInput + bias
up = ffn_gate_up_exps[e][:, nFf:] × moeInput + bias
gate = clamp(gate, -∞, 7)
up = clamp(up, -7, 7)
out_e = gate · sigmoid(α · gate) · (up + 1)
out_e = ffn_down_exps[e] × out_e + bias
─► weighted Σ out_e using router weights
─► residual += weighted_sum
After all layers:
hidden ─► narrow(seq_len-1) if prefill # GPT OSS narrows BEFORE MoE
# in the last layer, see § 8
─► RMSNorm(output_norm.weight, eps)
─► output.weight matmul → logits
─► copy to float[VocabSize]
- GQA with
Config.NumKVHeads < Config.NumHeads. - Optional fused QKV (
_isQkvFused) — if the GGUF ships a singleattn_qkv.weight,FuseQKVWeights()builds a fused tensor; otherwise the three linear ops are dispatched separately. - Bias on every projection: implemented in
LinearForwardWithBias(), which performs the matmul and then adds the bias row-by-row. The bias is looked up from_weights[biasName]. - No QK-norm: unlike Gemma / Qwen, GPT OSS skips the per-head Q and K normalization.
- Attention sinks: per-head bias
attn_sinks.weight(shape[numHeads]). Treated as a virtual token in the softmax: its value participates in both max-finding and exp-sum, effectively adding an always-available attention target. Implemented inApplySoftmaxWithSinks()andAttentionDecodeWithSinks(). - RoPE: NeoX-style with YaRN scaling.
Ops.RoPEExis called withorigCtxLen = Config.OriginalContextLength(4096),freqScale = 1 / RopeScale,beta_fast = 32,beta_slow = 1. - Attention pattern: even layers ⇒ SWA (window from
_slidingWindow, default 128); odd layers ⇒ full causal. (Implementation note: in the current code path all layers attend tototalSeqLen— the SWA bound is read from GGUF metadata but not yet used to bound the actual softmax width. This is on the optimization-opportunities list.)
gate = clamp(gate, -∞, SiluLimit) # SiluLimit = 7.0
up = clamp(up, -SiluLimit, SiluLimit)
out = gate · sigmoid(SiluAlpha · gate) · (up + 1) # SiluAlpha = 1.702
The constants SiluAlpha and SiluLimit are hardcoded. The activation is
implemented with SIMD vectorization in SiLUAlphaLimitInPlace and is also
exposed to the fused MoE prefill kernel as swiglu_oai.
- Every layer has
_numExpertsexperts (32 for gpt-oss-20b) with TopK routing (_numExpertsUsed = 4). - Routing is
linear(hidden) + bias → TopK → softmax(selected only). This is TopK-then-softmax, the inverse of Gemma 4's softmax-then-TopK flow. - Expert weights are stored as fused
gate ‖ uprows inffn_gate_up_exps.{E}.weight, with a matching fusedffn_gate_up_exps.{E}.bias.FuseExpertGateUpWeights()performs this fusion at load time. - Expert biases are stored in the GGUF as packed
[numExperts, biasDim]tensors (ffn_gate_exps.bias).SplitExpertBiases()unpacks these into per-expert[biasDim]tensors before the fusion step.
For prefill on a GGML backend, GPT OSS dispatches one
ggml_mul_mat_id + ggml_add_id + swiglu_oai graph per layer instead of
looping over active experts per token. The kernel reads:
- The original 3D
ffn_gate_exps.weight,ffn_up_exps.weight,ffn_down_exps.weightblocks (zero-cost views into the mmap'd model). - A contiguous
[2 * nFf, numExperts]f32 stacked bias for gate / up (built once byInitMoeStackedWeightsfrom the per-expert biases captured before fusion). - A contiguous
[hidden, numExperts]f32 stacked bias for down (also built once at init time).
Per-token routing is computed in C# (TopK + softmax) and the resulting
(token, expert) map is fed to the kernel.
GPT-4o BPE pre-tokenizer with \p{N}{1,3} number grouping. Handled by the
runtime tokenizer when tokenizer.ggml.pre == "gpt-4o".
| Key | Type | Meaning |
|---|---|---|
gptoss.expert_count |
uint32 | Number of experts (32) |
gptoss.expert_used_count |
uint32 | TopK experts per token (4) |
gptoss.attention.sliding_window |
uint32 | SWA window size (128) |
gptoss.expert_feed_forward_length |
uint32 | Expert FFN dim |
gptoss.rope.scaling.original_context_length |
uint32 | Yarn original context (4096) |
tokenizer.ggml.pre |
string | Pre-tokenizer type (gpt-4o) |
token_embd.weight
output_norm.weight
output.weight
blk.{L}.attn_norm.weight
blk.{L}.attn_q.weight / attn_q.bias
blk.{L}.attn_k.weight / attn_k.bias
blk.{L}.attn_v.weight / attn_v.bias
blk.{L}.attn_qkv.weight / attn_qkv.bias # post-fusion (when QKV is fused)
blk.{L}.attn_output.weight / attn_output.bias
blk.{L}.attn_sinks.weight # per-head bias [numHeads]
blk.{L}.post_attention_norm.weight
blk.{L}.ffn_gate_inp.weight / ffn_gate_inp.bias # router [numExperts, hidden]
blk.{L}.ffn_gate_up_exps.{E}.weight # fused expert gate+up
blk.{L}.ffn_gate_up_exps.{E}.bias # fused expert gate+up bias
blk.{L}.ffn_down_exps.{E}.weight # expert down
blk.{L}.ffn_down_exps.{E}.bias # expert down bias
Constructor (GptOssModel(string ggufPath, BackendType backend)):
ParseBaseConfig().- Reads MoE counts, the SWA window, expert FFN length, and the YaRN original context length.
ParseTokenizer().LoadWeights().SplitExpertBiases()— unpacks the GGUF's packed expert bias tensors.- Snapshot the gate / up biases per expert before fusing. The fused MoE prefill kernel needs the biases in their original split shape to build a contiguous stacked bias table.
FuseExpertGateUpWeights()— concatenates each expert's gate and up intoffn_gate_up_exps.{E}.weightand the matching fused bias.FuseQKVWeights().PrepareCudaQuantizedWeightsForInference().InitKVCache(maxSeqLen).PrecomputeConstants()— pre-allocates per-layer weight name arrays and per-expert name arrays so the hot loops do no string interpolation.InitMoeStackedWeights(preFuseGateBias, preFuseUpBias)— builds the stacked biases used by the fused MoE prefill kernel. Also resolves the per-layerStackedExpertWeightsviews into the original 3D_exps.weightblocks.
Forward(int[] tokens) then runs the per-op managed loop:
- Embedding lookup.
- For each layer:
LinearForwardWithBiasfor Q / K / V, RoPE,AttentionDecodeWithSinks(orApplySoftmaxWithSinksfor prefill), output projection + bias, MoE block (or fused MoE prefill kernel). - For the last transformer layer in prefill, the residual is narrowed to the last token before MoE (an early-exit optimization that skips the MoE computation on positions that do not feed the LM head).
- Final RMSNorm, LM head, copy to
_logitsBuffer.
- Last-layer narrow before MoE. Inside the last
TransformerBlock,moeInputis narrowed to[1, hidden]so the MoE block only computes the one row that matters for the LM head. All earlier layers compute the MoE branch over the full sequence. - Stacked MoE prefill kernel (
TryMoEPrefillFused). Onemul_mat_id + add_id + swiglu_oaigraph dispatch per layer for the routed-expert FFN, replacing per-token per-expert dispatches. See § 4.4. - Pre-built per-layer name arrays (
_layerNames[L][]) and per-expert name arrays (_expertNames[L][E][]) eliminate string interpolation in the hot loops. - Cached attention sinks in
_layerSinks[L][numHeads]so the softmax doesn't need to re-fetch the sinks every step.
- Per-step routing buffers (
_moeExpertCounts,_moeExpertOffsets,_moeTokenMap,_moeWeightMap) are reused across tokens. - SIMD-vectorized bias addition and SiLUAlphaLimit activation in
LinearForwardWithBiasandSiLUAlphaLimitInPlace. - Attention sinks softmax runs on CPU (scalar with optional SIMD on the exp-sum). The fused GPU kernel for sinks is on the optimization opportunities list — currently the softmax with sinks is the slow path on Metal / CUDA.
- MXFP4 expert weights stay quantized in
_quantWeights; matmul is dispatched through the backend's quantized matmul.
There is no whole-model native decode path for GPT OSS (unlike Qwen 3), so decode dispatches are still per-op. This is the largest open optimization target.
- Per-layer K and V tensors of shape
[NumKVHeads, maxSeqLen, headDim]. KV dtype is configurable asf32,f16, orq8_0. ResetKVCache()zeroes everything.- The expert FFN weights live in the original 3D
ffn_gate_exps.weight/ffn_up_exps.weight/ffn_down_exps.weightblocks loaded byModelBase.FuseExpertGateUpWeights()only disposes the per-expert views, not the underlying bulk buffer. This means the fused MoE prefill kernel can still address the original blocks directly via_layerStackedGate/_layerStackedUp/_layerStackedDown.
GPT OSS implements IBatchedPagedModel.ForwardBatch
(GptOssModel.BatchedForward.cs)
and runs it by default — concurrent requests can only be served truly
in parallel through the batched path (the per-sequence fallback forwards
at most one sequence per step). Set TS_GPTOSS_BATCHED=0 to force the
legacy per-sequence KV-swap fallback for A/B comparison. The batched port has to
preserve GPT OSS's three architecture-distinguishing features —
attention sinks, bias on every projection, and per-layer
alternating SWA — inside the paged scheduling stack:
- Per-layer paged K/V of layout
[numBlocks * blockSize * numKvHeads * headDim], lazily grown copy-on-write. - Batched QKV with bias per token; NeoX + YaRN RoPE is dispatched
with an explicit
positions[]array. - Per-layer SWA window — the same alternating local / global window pattern as the legacy path is passed per-call into the paged kernel, so the batched scheduler still sees one uniform model while each layer respects its own attention horizon.
- Native paged attention with per-head sinks:
TSGgml_PagedAttentionForwardWithSinks(ggml_ops_paged_attention.cpp) combinesggml_flash_attn_extwith theadd_sinksvariant so the sink logits participate in softmax normalization without contributing to V — the same numerical behaviour as the legacy CPU sinks softmax. Exposed throughGgmlBasicOps.PagedAttentionForwardWithSinks.- The managed C# fallback,
ManagedPagedAttention.ForwardWithSinks, is selected when running on non-GGML backends or whenTS_GPTOSS_PAGED_ATTN_MANAGED=1forces the C# path. Both produce bit-identical greedy output.
- The managed C# fallback,
- MoE FFN runs through the existing
MoEForward(numTokens)token-parallel path; no GPT-OSS-specific batched MoE kernel.
- 100% greedy match vs legacy (12/12 tokens) in
GptOssBatchedCorrectnessTests— preserved across both the managed sinks fallback and the native sinks kernel. - Throughput remains flat (~0.93–1.05× across n=1, 3, 5 in
GptOssBatchedPerfBench). The native sinks kernel didn't move the needle because the legacy per-seq path already runs one fused per-layer kernel (TryFusedAttnLayerPrefill: RMSNorm + fused QKV + RoPE + KV-append- sinks-aware softmax + attn + output proj + residual in one cgraph), whereas the batched path issues ~5 separate graphs per layer (norm, QKV, RoPE, paged-attn, output, residual). Closing this gap needs a fused-per-layer batched kernel for GPT OSS — substantial follow-up work.
- Harmony format is non-optional. The parser (
HarmonyOutputParser) is markedAlwaysRequired = truebecause the model always wraps its output in channel tags:<|channel>analysis ...for chain-of-thought reasoning.<|channel>final ...for the user-visible answer.
- The output parser strips the
<|channel>analysis ...block (or surfaces it as<think>content for the API) and exposes the<|channel>finalpayload as the assistant message. - Tool calling is supported via the Harmony
commentarychannel. When the request includestools:- The system message gains the line "Calls to these tools must go to the
commentary channel: 'functions'." and the developer message gains a
# Toolsblock declaring each tool as a TypeScript namespace (namespace functions { type NAME = (_: { ... }) => any; }). - The model emits a call as
<|channel|>commentary to=functions.NAME <|constrain|>json<|message|>{args}<|call|>.HarmonyOutputParserparses the channel +to=functions.NAMErecipient and decodes the JSON arguments into aToolCall(max one call per turn, matching the reference implementations). - Tool results are fed back as
<|start|>functions.NAME to=assistant<|channel|>commentary<|message|>{result}<|end|>. - Stop tokens: a tool call terminates with
<|call|>(id 200012), which the gpt-oss GGUF does not list as an eos (only<|return|>, id 200002, is).ModelBaseadds<|call|>to the stop set for thegptoss/gpt-ossarchitectures so generation halts after the call and it can be parsed. - Round-trip validated against gpt-oss-20b in
HarmonyToolCallIntegrationTests; format/parse coverage inHarmonyToolCallTests.
- The system message gains the line "Calls to these tools must go to the
commentary channel: 'functions'." and the developer message gains a
- Chat template uses the GPT-4o pre-tokenizer. Rendering is handled by the
hardcoded
ChatTemplate.RenderHarmony(the GGUF's embedded Harmony Jinja relies on recursive macros /namespace()/strftime_nowthat the lightweight Jinja2 engine does not fully support, especially on the tool path), so gpt-oss is routed to the hardcoded renderer likemistral3andnemotron_h.
- Fused per-layer batched kernel — the legacy path's
TryFusedAttnLayerPrefilldoes one cgraph per layer (norm + fused QKV + RoPE + KV-append + sinks-aware softmax + attn + output proj + residual). The batched path currently issues ~5 graphs per layer. Folding them into one batched cgraph is the largest remaining win for batched GPT OSS perf. - Fused QKV in every release — when the GGUF ships split Q / K / V the
per-projection bias add still happens after each separate matmul. A
FusedQKVWithBiasgraph would cut 3 dispatches into 1. - Native whole-model decode (legacy) — like Qwen 3's
TransformerModelDecode, with attention sinks and bias support, would remove most managed overhead on the per-seq path. - GPU-fused sinks softmax (legacy) — the legacy CPU sinks softmax is
the per-seq counterpart to the batched native
*_WithSinkskernel. A custom Metal / CUDA fused kernel for the legacy path would close that gap and let the per-seq path keep up at single-sequence workloads. - Per-expert decode batching — even with the stacked MoE prefill
kernel, decode still runs experts sequentially per token. A batched
decode path (analogous to Qwen 3.5's
MoEExpertsSwiGLUResidual) would collapsenumExpertsUseddispatches into one. - SWA bound on even layers —
_slidingWindowis read from GGUF metadata but the current attention path does not yet bound the softmax width to it. Wiring it throughAttentionDecodeWithSinkswould lower per-token compute on long contexts.