Safe By Design AI

Findings

Things worth knowing before you read a Granite configuration as though it were a description of the model. These are not claims about model quality; they are places where the metadata says something other than what a careful reader would take it to mean, and each one is a trap somebody has to walk into once.

Every count below is recomputed from the published configurations and their digests each time this page is generated.

1. The class name does not always describe the stack

7 models declare GraniteMoeHybridForCausalLM and then list every layer as attention. There is no state-space layer in them. The hybrid class is the configuration family they were cut from, not a statement about what is in the model.

modellayersstack as listed
granite-4.0-1b4040 attention
granite-4.0-1b-base4040 attention
granite-4.0-350m2828 attention
granite-4.0-350m-base2828 attention
granite-4.0-micro4040 attention
granite-4.0-micro-base4040 attention
granite-guardian-4.0-3b-toxicity-ja4040 attention

Read the layer table, not the class name. The published layer_types table is the reliable fact; grouping on the class name puts pure-attention models in the family whose whole point is that it is not one. Each of the first six shares its class and its nominal size with a hybrid twin, but not its geometry: granite-4.0-1b is 40 x 2048 where granite-4.0-h-1b is 40 x 1536, granite-4.0-350m is 28 x 1024 where granite-4.0-h-350m is 32 x 768, and granite-4.0-micro is 40 x 2560 where granite-4.0-h-micro is 40 x 2048. The class name does not separate them; the layer table does. Nor does the model name settle it the other way: granite-4.0-tiny-preview and granite-4.0-tiny-base-preview carry no -h- and are hybrid all the same.

So the family grouping in models.md follows the layer table, and a sheet's shape includes its stack as well as its class, depth and width (method §4). The pairs are set out in generations.md.

2. Keys left behind by the template

11 models carry mamba_* geometry keys — state dimensions, convolution widths — with no state-space layer to use them. 17 declare num_local_experts: 0, an expert count on a model that has no experts.

Read literally, a reader is told a model is a mixture of experts with zero of them. Read as what it is — a field the template carried and this checkpoint does not use — it is harmless. A tool that branches on presence rather than on value gets this wrong.

3. One fact, several spellings

Granite spans several upstream codebases and each brought its own vocabulary for the same number.

factspellingmodels
depthnum_hidden_layers118
depthn_layer9
depthnum_layers5
depthencoder_num_layers1
widthhidden_size117
widthd_model8
widthn_embd7
widthembedding_feature_dim1
contextmax_position_embeddings117
contextn_positions7

Reading only the common spelling silently loses every model of the older code lineage. Depth is not always num_hidden_layers.

4. The geometry is not always in the same place

wheremodels
at the top level116
nowhere — no configuration published33
under text_config15
published, but no depth field6
under encoder_config2

A model with a language half nests under text_config. One without — an encoder that emits tokens directly — nests under encoder_config and has no text config at all. Looking in one place finds most models and misses a family.

5. Multi-query attention is declared by a flag

7 models set multi_query: true and publish no num_key_value_heads. Every query head shares one key/value head. A reader that infers grouping only from a head count records these as having no grouping at all, which is the exact opposite of the truth.

modelheadskey/value heads
granite-20b-code-base-8k481 (by flag)
granite-20b-code-base-r1.1481 (by flag)
granite-20b-code-instruct-8k481 (by flag)
granite-20b-code-instruct-r1.1481 (by flag)
granite-20b-functioncalling481 (by flag)
granite-34b-code-base-8k481 (by flag)
granite-34b-code-instruct-8k481 (by flag)

6. Many checkpoints are indistinguishable by configuration

16 groups of models publish a byte-identical config.json — the same sha256 in evidence/claims.json, so a reader can confirm each group from the digests alone. A base and an instruction-tuned model, a code base and its instruct variant, a revision and its predecessor, even two consecutive releases, are one architecture; the weights differ and nothing in the configuration says so. Nor does a shared configuration promise a shared tensor set: granite-4.1-8b and granite-4.1-8b-base publish the same file byte for byte, and only the first ships a separate lm_head.weight (§10).

models sharing one configuration
GneissWeb.7B_ablation_model_on_350B_FineWeb.Edu.seed1, GneissWeb.7B_ablation_model_on_350B_FineWeb.Edu.seed2, GneissWeb.7B_ablation_model_on_350B_FineWeb.Edu.seed3, GneissWeb.7B_ablation_model_on_350B_FineWeb.seed1, GneissWeb.7B_ablation_model_on_350B_FineWeb.seed2, GneissWeb.7B_ablation_model_on_350B_FineWeb.seed3, GneissWeb.7B_ablation_model_on_350B_GneissWeb.seed1, GneissWeb.7B_ablation_model_on_350B_GneissWeb.seed2, GneissWeb.7B_ablation_model_on_350B_GneissWeb.seed3
granite-20b-code-base-8k, granite-20b-code-base-r1.1
granite-20b-code-instruct-8k, granite-20b-code-instruct-r1.1
granite-3.1-2b-instruct, granite-3.2-2b-instruct
granite-3b-code-base-2k, granite-3b-code-instruct-2k
granite-4.0-1b, granite-4.0-1b-base
granite-4.0-350m, granite-4.0-350m-base
granite-4.0-h-1b, granite-4.0-h-1b-base
granite-4.0-h-350m, granite-4.0-h-350m-base
granite-4.0-h-micro, granite-4.0-h-micro-base
granite-4.0-h-tiny, granite-4.0-h-tiny-base
granite-4.0-micro, granite-4.0-micro-base
granite-4.1-3b, granite-4.1-3b-base
granite-4.1-8b, granite-4.1-8b-base
granite-8b-code-base-4k, granite-8b-code-instruct-4k
granite-speech-5.0-470m-turboctc, granite-speech-5.0-470m-turboctc-nc

These are the groups that match byte for byte, provenance fields included. Setting aside fields that record how a file was written rather than what the model is — _name_or_path, transformers_version, torch_dtype — can only merge more: safety classifiers and adapter libraries that repeat their base's geometry are candidates. This bundle carries digests, not whole configurations, so it does not list those further groups rather than list them unchecked.

This is why nothing in this reference tries to pick "the" model for an architecture, and why the role column in models.md is derived from the name — it is the only place that distinction is recorded.

7. Context lengths are not all round numbers

tokensmodelsnote
131,07261= 128 x 1024
8,19224
4,09617
5147= 512 + 2 (RoBERTa-style position offset); every one is a RoBERTa or XLM-RoBERTa model
32,7686
128,0004= 125 x 1024, and universally called 128k
2,0482
5122
16,3841

Both values get called "128k". They differ by 3,072 tokens. This reference prints the number the configuration holds and leaves the naming alone.

8. Not every model publishes a configuration, and not every configuration has a depth

33 of 172 models have no config.json at all: adapters that ship only adapter weights, earth-observation releases with their own formats, and dataset-ablation artifacts. They are real published models and they cannot be described by the method used here, which is why they appear in models.md with dashes rather than being dropped.

A further 6 publish a configuration that simply has no layer count — the speculative-decoding heads, whose size is how many tokens they propose (n_predict) rather than how many layers they stack. A published configuration without a depth field is not a missing one, and counting the two together overstates how much of this catalogue is undocumented.

9. A global rope base lower than the local one

ModernBERT-class configurations carry two rope bases: global_rope_theta for the layers that attend over the whole input (every third layer, global_attn_every_n_layers: 3) and local_rope_theta for the rest, which attend within 64 positions either side (local_attention: 128) (modeling_modernbert.py at transformers v4.57.1, L464–L471). In three of the five Granite models of this class the global base is the larger one, as the longer reach of those layers would suggest. In the two multilingual models it is the other way round: global_rope_theta is 150,000 and local_rope_theta is 160,000.

modelclasscontextglobal baselocal base
granite-embedding-311m-multilingual-r2ModernBertModel32,768150,000160,000
granite-embedding-97m-multilingual-r2ModernBertModel32,768150,000160,000
granite-embedding-english-r2ModernBertModel8,19280,00010,000
granite-embedding-small-english-r2ModernBertModel8,19280,00010,000
granite-embedding-reranker-english-r2ModernBertForSequenceClassification8,192160,00040,000

These are the values the configurations publish, and the modelling code applies each to the layers its name says. Nothing here says whether the multilingual values were intended; the point is that a reader who assumes the global base is the larger of the two, or who reads one rope_theta for the whole model, gets these two checkpoints wrong.

10. The configuration does not always describe the shipped tensors

A configuration says what a loader should build; the weight files say what was saved. The two were compared for every model that publishes both a configuration and readable safetensors headers — 135 of them — using the tensor inventory in evidence/tensors.json (how it was built is under provenance). Declared depth, width, vocabulary, query and key/value heads, expert counts and Mamba projection widths agree with the tensor shapes everywhere. The one depth gap is expected: the three granite-switch-4.1 previews declare one layer more than they ship, and that layer is the switch slot, which holds no weights. What does not agree:

modelthe configuration saysthe shipped tensors showrevision
granite-3.0-1b-a400m-basetie_word_embeddings: truea separate lm_head.weight [49152, 1024]d91cbed
granite-3.0-2b-instructtie_word_embeddings: truea separate lm_head.weight [49155, 2048]5ad66c1
granite-3.0-3b-a800m-basetie_word_embeddings: truea separate lm_head.weight [49152, 1536]142d2c8
granite-3.0-3b-a800m-instructtie_word_embeddings: truea separate lm_head.weight [49155, 1536]51ced9b
granite-4.1-8btie_word_embeddings: truea separate lm_head.weight [100352, 4096]1504002
granite-vision-3.3-2b-chart2csv-previewtie_word_embeddings: true, at the top level and in text_configa separate language_model.lm_head.weight [49156, 2048]298ed4d
granite-4.0-3b-visiontie_word_embeddings: true, at the top level and in text_configa separate lm_head.weight [100353, 2560]bf108f3
granite-speech-3.2-8bGraniteSpeechForConditionalGeneration, whose encoder in transformers v4.57.1 is encoder.input_linear and encoder.layers.#encoder tensors under a different layout: encoder.rnn_tr.#, with attn.fn.to_q, ff1.fn.fn.net.# and conv.net.# inside each blockd583502
granite-speech-3.3-2bone 40-layer decodertwo complete shard sets side by side: the 4-file set that model.safetensors.index.json references (6.1 GB) and an older 3-file set (5.7 GB) that nothing references; a loader that follows the index reads only the 4-file set4ac2f02
granite-guardian-3.2-5b-lora-harm-correctiona complete GraniteForCausalLM, 28 × 4,096only adapter_model.safetensors: rank-32 LoRA factors on the query, key and value projections of 28 layers, and no base weights9d20ea5

What a loader does with each.

Parameter counts. The published totals match the sum of tensor elements to within stored integer counters (num_batches_tracked, and the granite-switch adapter lookup tables), except in four models. For granite-4.0-3b-vision and granite-vision-3.3-2b-chart2csv-preview the published total is the tensor sum minus exactly the lm_head.weight elements (256,903,680 and 100,671,488). For granite-speech-3.3-2b it matches the indexed shard set; the unreferenced older set is left out. For granite-speech-4.1-2b-plus the headers hold 65,536 more elements than the published total, and nothing in them explains the gap. Adapter files shipped beside a checkpoint are left out of these sums.

So a configuration is a statement of what the model should be, and the published weights are what a loader actually has to reconcile with it. Where the two disagree, the pages here report the configuration and point here.