Safe By Design AI

GraniteSpeechForConditionalGeneration

GraniteSpeechForConditionalGeneration at 40 layers and hidden size 2048. 3 published checkpoints share this shape.

Implementing it

How to read this sheet: method.

This section is not the recorded pass (that is on passes); it is what the published configuration and the modelling code say the arithmetic is.

From granite-4.0-1b-speech

Also covers granite-speech-4.1-2b.

# Mel features in, tokens out. The encoder is a conformer; what sits on
# top of it is what distinguishes the speech classes from each other.

x = input_linear(features)                # [T, 1024]
for i in 0 .. 15:                         # conformer block, five modules in this order
    x = x + 0.5 * ff1(x)                  # pre_norm -> up_proj -> silu -> down_proj
    x = x + to_out(attn_blocked(pre_norm(x)))   # defined below
    x = x + conv(x)                       # LayerNorm, pointwise, GLU,
                                          #   depthwise, BatchNorm, silu,
                                          #   pointwise
    x = x + 0.5 * ff2(x)                  # the second half-weight FFN
    x = post_norm(x)
    if i == 7:                          # after block 8 of 16: self-conditioning
        x = x + out_mid(softmax(out(x)))  # the CTC head's own distribution,
                                          # fed back into the residual

#   The conformer tower, as published:
#     layers         16   `num_layers`
#     width        1024   `hidden_dim`
#     heads           8   `num_heads`
#     head dim      128   `dim_head`
#     ffn mult        4   `feedforward_mult`
#     conv kernel    15   `conv_kernel_size`
#     output dim    348   `output_dim`

# attn_blocked, used once per block above. Blocked, not full: frames are
# split into non-overlapping 200-frame windows (the tail is zero-padded
# and masked), attention is block-diagonal, no window reads another.
def attn_blocked(h):
    out = empty_like(h)
    for w, o in zip(windows(h, 200), windows(out, 200)):
        q = to_q(w); k, v = split(to_kv(w), 2)
        b = (q @ k.T + q @ rel_pos_emb[dist].T) / sqrt(head_dim)
        o[:] = softmax(b) @ v             # Shaw relative positions: a
                                          #   learned vector per clamped
                                          #   offset, dotted with q and
                                          #   added to the scores. No rope
                                          #   anywhere in this tower.
    return out

enc = x                                   # the final residual, only

for each window of 15 encoder rows:        # windowed Q-Former, BLIP-2 block
    q = layernorm(query)                  # 3 learned queries [3, 1024], reused
                                          #   for every window, LayerNorm'd once
                                          #   before layer 0
    for layer in 0 .. 1:
        q = self_attn(q); q = cross_attn(q, window); q = ffn(q)   # post-norm
# The tail window is zero-padded to a full window, not truncated, and
# the processor sizes the audio placeholder run for the padded count.
rows = linear(q)                          # 3 rows per window, decoder-wide

#   The Q-Former projector, as published:
#     layers             2   `num_hidden_layers`
#     width           1024   `hidden_size`
#     heads             16   `num_attention_heads`
#     ffn width       4096   `intermediate_size`
#     activation      gelu   `hidden_act`
#     norm eps       1e-12   `layer_norm_eps`

x = embed[ids]; x[at_the_audio_placeholder] = rows

# The decoder is an ordinary dense Granite, and its four multipliers
# apply at different points in the pass.

x = x * 12                                # embedding_multiplier, once, on the way in
for i in 0 .. 39:
    h = rmsnorm(x, layers[i].input_layernorm, eps=1e-05) # pre-norm
    q, k, v = h @ layers[i].self_attn.q_proj.T, h @ layers[i].self_attn.k_proj.T, h @ layers[i].self_attn.v_proj.T # 16 q heads, 4 kv
    q, k = rope(q, k, theta=10000)     # before the scores
    s = q @ k.T * 0.0078125               # attention_multiplier replaces
                                          #   1/sqrt(head_dim), not a factor
                                          #   applied beside it
    a = softmax(s, mask=causal)
    o = (a @ v) @ layers[i].self_attn.o_proj.T
    x = x + 0.22 * o                      # residual_multiplier on both
    h = rmsnorm(x, layers[i].post_attention_layernorm)
    m_ = (silu(h @ layers[i].mlp.gate_proj.T) * (h @ layers[i].mlp.up_proj.T)) @ layers[i].mlp.down_proj.T
    x = x + 0.22 * m_                     #   ...and on the MLP add too

x = rmsnorm(x, model.norm)
logits = x @ lm_head.T
logits = logits / 8                       # logits_scaling divides

granite-speech-3.3-2b: differs from the block above

Same arithmetic; only these values change.

granite-4.0-1b-speech, granite-speech-4.1-2bgranite-speech-3.3-2b
decoder heads (q / kv)16 / 432 / 8
decoder FFN width4,0968,192
rope theta10,00010,000,000
attention_multiplier0.00781250.015625
output headlm_head (tie_word_embeddings: false)the input embedding (tie_word_embeddings: true)
vocabulary100,35349,160
encoder output_dim348256

Sources, transformers v4.57.1: the conformer block, its feed-forward, block attention with Shaw positions and conv module; the encoder loop and self-conditioning; the windowed Q-Former projector and the BLIP-2 Q-Former's LayerNorm on the queries before layer 0; the placeholder count; the Granite decoder's attention_multiplier, residual_multiplier, embedding_multiplier and logits_scaling.

Notes that apply to more than one block

The conformer's five modules run in that order and the two feed-forwards are half-weighted — each contributes 0.5 * to the residual. That factor is structural, carries no tensor, and is invisible in a weight listing.

The tower never runs a full attention. Frames are split into non-overlapping context_size: 200-frame windows and attention is block-diagonal — no window reads another. Inside a window the positions are Shaw relative: attn.rel_pos_emb holds a learned vector per clamped offset, dotted with the query and added to the scores. There is no rope in this tower; full attention with rope is a different function.

Self-conditioning fires once, after the middle block (num_layers // 2, counting from 1): the mid-stack CTC head's distribution is projected back into the residual stream through out_mid. A tower that skips it is a different function.

The Q-Former's last window is zero-padded to a full window rather than truncated, and the processor counts ceil(frames / 15) * 3 audio placeholders, so every row the projector emits — the padded window's included — has a slot. Truncating instead loses the end of every clip that does not divide evenly.

Geometry

layers40
hidden size2,048
attention heads32, 8 key/value (granite-speech-3.3-2b); 16, 4 key/value (granite-4.0-1b-speech, granite-speech-4.1-2b)
feed-forward width8,192 (granite-speech-3.3-2b); 4,096 (granite-4.0-1b-speech, granite-speech-4.1-2b)
vocabulary49,160 (granite-speech-3.3-2b); 100,353 (granite-4.0-1b-speech, granite-speech-4.1-2b)
trained context131,072 tokens (granite-speech-3.3-2b); 4,096 tokens (granite-4.0-1b-speech, granite-speech-4.1-2b)
largest checkpoint3B

Weight structure

The tensors one element of the repeating stack holds, by the names the published checkpoint uses.

repeating stackdepthwhat one element holds
encoder.layers.#16attn.pre_norm.bias, attn.pre_norm.weight, attn.rel_pos_emb.weight, attn.to_kv.weight, attn.to_out.bias, attn.to_out.weight, attn.to_q.weight, conv.batch_norm.bias, conv.batch_norm.num_batches_tracked, conv.batch_norm.running_mean, conv.batch_norm.running_var, conv.batch_norm.weight, conv.depth_conv.conv.weight, conv.down_conv.bias, conv.down_conv.weight, conv.norm.bias, conv.norm.weight, conv.up_conv.bias, conv.up_conv.weight, ff1.down_proj.bias, ff1.down_proj.weight, ff1.pre_norm.bias, ff1.pre_norm.weight, ff1.up_proj.bias, ff1.up_proj.weight, ff2.down_proj.bias, ff2.down_proj.weight, ff2.pre_norm.bias, ff2.pre_norm.weight, ff2.up_proj.bias, ff2.up_proj.weight, post_norm.bias, post_norm.weight

granite-speech-3.3-2b's repository holds two shard sets: the 4-file set its model.safetensors.index.json references, and an older 3-file set (5.7 GB) that nothing references. A loader that follows the index reads only the 4-file set; a tool that globs *.safetensors sees every tensor twice (findings §10).

Other shapes of GraniteSpeechForConditionalGeneration

Identical across all of them: layers 40.

Only the columns that differ are shown; a cell with several values means the checkpoints of that shape disagree.

shapewidthheadsKV headsFFN widthvocabularycontextcheckpoints
40L x 4,0964,09632812,80049,156 / 49,160131,0722
40L x 2,048 (this sheet)2,04816 / 324 / 84,096 / 8,19249,160 / 100,3534,096 / 131,0723

How this was checked

Status and evidence grade in the assessment (see what the grades mean):

The implementation meant here and below is an unpublished independent inference implementation by the author.

What stands behind the block above, beyond the published configuration it is read from:

Where an independent implementation and the published configuration disagree, the configuration is what this page reports and the disagreement is what it says.

Checkpoints with this architecture

modelparameterscontextheadsFFN widthvocabulary
granite-speech-3.3-2b3B131,07232, 8 key/value8,19249,160
granite-4.0-1b-speech2.3B4,09616, 4 key/value4,096100,353
granite-speech-4.1-2b2.3B4,09616, 4 key/value4,096100,353

Recorded pass: passes.