Granite NAR (NLE Editor)
June 28, 2026 · View on GitHub
Status: research
Identity
- Family key:
granite_nar - Upstream architecture string:
nle(NLENARDecoder) - Hugging Face repo:
ibm-granite/granite-speech-4.1-2b-nar(pinned99a4df9007ac5682f9daa093fb7008ff606e9a5d) - License: Apache-2.0
- Variants:
granite-speech-4.1-2b-nar: Non-autoregressive editor; en/fr/de/es/pt transcription only.
This is a sibling release to the granite audio-LLM family but with a fundamentally different decoding pipeline (encoder-CTC plus bidirectional Granite LLM as a single-pass editor). Kept in its own family so the transcribe.cpp src/arch/granite_nar/ implementation does not need to share code with the AR audio-LLM family.
References
- Canonical reference:
transformerswithtrust_remote_code=True. The repo ships its ownconfiguration_nle.py,feature_extraction_nle.py, andmodeling_nle.py(registered viaauto_map); no IBM author PyPI package exists. Model card pinstransformers==4.57.6or5.5.3, plusflash-attn==2.8.3. - Instrumented reference: same as canonical. Configured under
scripts/envs/granite_nar/at Stage 2. - Cross-check references: the AR sibling family (
granite) shares the underlying Conformer encoder and the Granite-4.0-1b-base LLM weights; encoder-side tensor-name conventions can be reused, but the LLM operates without a causal mask here so its forward graph differs.
Architecture
Pattern: encoder-ctc with a bidirectional-LLM editor head on top. The four canonical porting patterns do not directly capture the editor step, so Stage 4 will need a custom flow.
- Audio encoder: Conformer with block-attention, identical dims to the AR
granite_speechencoder: 16 layers,hidden_dim=1024, 8 heads atdim_head=128, depthwiseconv_kernel_size=15,conv_expansion_factor=2,feedforward_mult=4,input_dim=160(= 80 logmels with a 2-frame stack),output_dim=348,max_pos_emb=512(Shaw relpos),context_size=200. Adds NAR-specific extras:bpe_output_dim=100353,bpe_pooling_window=4,self_conditioning_layer=8,attn_type=block,loss_lambda=0.2. - Projector:
nle_projector(NOT a Q-Former). MLP-with-attention module:block_size=15,downsample_rate=5,encoder_dim=1024,hidden_size=2048,llm_dim=2048,mlp_ratio=2,num_layers=2,num_heads=32,num_encoder_layers=4,attn_bias=true,layernorm_eps=1e-06. Consumes multi-layer encoder features atencoder_layer_indices=[4, 8, 12, -1].scale_projected_embeddings=true(the projector output is scaled before the LLM; exact factor lives in modeling_nle.py and must be read at oracle time). - Text LM:
granite-4.0-1b-base(model_type=granite), same dims as in the AR family (40 layers, 2048 hidden, 16 query / 4 KV heads, RMS eps 1e-05, RoPE theta 10000, SiLU, Granite scalar multipliers). Differences here:tie_word_embeddings=true(vs false in the base AR variants).vocab_size=100352(vs 100353 in AR; noaudio_token_indexbecause there is no LLM token-injection).- Causal mask disabled:
attn_implementation=flash_attention_2withis_causal=False. The LLM is used as a bidirectional editor; the ggml graph must not apply the standard causal triangular mask. - LoRA adapters are hinted by the model card but
has_lora_adapteris unset in config. Stage 3 (convert) must determine whether deltas are merged into the safetensors or shipped separately.
- Fusion: no audio-token scatter. The LLM receives projected encoder outputs directly as input embeddings.
- Decode: a single forward pass through encoder + projector + bidirectional LLM produces logits over the LLM vocab; CTC greedy decode on those logits yields the final text. No autoregressive token loop, no generation_config.
- CTC tokenizer: a 348-entry
char2idxembedded inline in config covering ASCII 32-127 + Latin-1 supplement 128-255 + Katakana 0x30A1-0x30FB (indices 256-347). Used at the encoder-side CTC head; the LLM-output CTC decode uses the same tokenizer as the AR variants (BPE, vocab 100352, identical sha256). The Katakana entries appear vestigial since Japanese is not in the supported language list.
Frontend
NLEFeatureExtractor with flat preprocessor_config.json fields (no melspec_kwargs nesting). Numerically identical to the AR granite_speech variants:
sample_rate=16000, mono.n_mels=80,hop_length=160,win_length=400,n_fft=512(window zero-padded to 512 before FFT).- Window/center/padding/mel-norm not declared; values follow
torchaudio.transforms.MelSpectrogramdefaults (Hann periodic,center=True, reflect padding, htk-style mel scale, no filter normalization). Stage 2 (oracle) must confirm by running the reference processor end-to-end.
The encoder consumes 80 mels x 2-frame stack = input_dim=160; the 2-frame stack is implicit in the encoder definition.
Commands
Reference WER run (create_causal_mask is monkey-patched to None inside
the runner so eager attention matches the upstream flash-attn-2
bidirectional semantics without requiring flash-attn-2 on the host):
uv run --project scripts/envs/granite_nar \
scripts/wer/run_reference_granite_nar_transformers.py \
--model ibm-granite/granite-speech-4.1-2b-nar \
--manifest samples/wer/test-clean.manifest.jsonl \
--out reports/wer/granite-speech-4.1-2b-nar-REF.test-clean.jsonl
Reference tensor dumps (encoder + projector + bidirectional decoder, single pass):
uv run --project scripts/envs/granite_nar \
scripts/dump_reference_granite_nar_transformers.py encoder \
--model ibm-granite/granite-speech-4.1-2b-nar \
--audio samples/jfk.wav \
--out build/validate/granite_nar/granite-speech-4.1-2b-nar/jfk/encoder/ref
uv run --project scripts/envs/granite_nar \
scripts/dump_reference_granite_nar_transformers.py decode \
--model ibm-granite/granite-speech-4.1-2b-nar \
--audio samples/jfk.wav \
--out build/validate/granite_nar/granite-speech-4.1-2b-nar/jfk/decode/ref
Conversion (preserves source bf16 dtype):
uv run --project scripts/envs/granite_nar \
scripts/convert-granite_nar.py ibm-granite/granite-speech-4.1-2b-nar \
--repo-id ibm-granite/granite-speech-4.1-2b-nar
Validation (ref-dtype, CPU strict, 11 tensors):
uv run scripts/validate.py all --family granite_nar --variant granite-speech-4.1-2b-nar
Benchmarks:
uv run scripts/perf/bench.py \
--model models/granite-speech-4.1-2b-nar/granite-speech-4.1-2b-nar-Q8_0.gguf \
--backend metal \
--machine "$(uname -srm | tr ' ' _)"
Full WER sweep (Stage 7):
for PRESET in BF16 F16 Q8_0 Q6_K Q5_K_M Q4_K_M; do
uv run scripts/wer/run.py \
--model models/granite-speech-4.1-2b-nar/granite-speech-4.1-2b-nar-${PRESET}.gguf \
--manifest samples/wer/test-clean.manifest.jsonl \
--out reports/wer/granite-speech-4.1-2b-nar-${PRESET}.test-clean.jsonl
uv run scripts/wer/score.py reports/wer/granite-speech-4.1-2b-nar-${PRESET}.test-clean.jsonl
done
Capability Validation
Allowed statuses: PASS, SKIP - not exposed by runtime, ACCEPTED GAP - <reason>.
| Capability | Mode | Command / test | Expected observable | Status |
|---|---|---|---|---|
| Transcribe | explicit language hint | build/bin/transcribe-cli -m models/granite-speech-4.1-2b-nar/granite-speech-4.1-2b-nar-BF16.gguf --language en samples/jfk.wav | non-empty plausible English transcript produced by a single NAR editor pass | PASS — emits "and so, my fellow americans, ask not what your country can do for you. ask what you can do for your country." (reference NLE has a typo "americ.ans"; C++ is cleaner) |
| Transcribe | auto / no language hint | build/bin/transcribe-cli -m models/granite-speech-4.1-2b-nar/granite-speech-4.1-2b-nar-BF16.gguf samples/jfk.wav | non-empty plausible transcript | PASS — same transcript as the language-hinted run (NLE has no language conditioning at the model API) |
| Translate | --target-language fr/de/es/pt/ja | not exercised | reference NAR variant does NOT advertise translation in upstream config | SKIP — not exposed by NAR family (the NLE editor is single-task ASR; AR granite_speech exposes translation) |
| Timestamps | word/segment | not exercised | NAR encoder collapses to per-window pooled output; no per-token timing recovery | SKIP — not exposed by runtime |
Notes
- Single-variant family at intake time. Future NAR releases from IBM would slot in here.
- Upstream LibriSpeech test-clean WER target for porting-7-wer: 1.29 (ref-dtype C++ must score <= 1.30 on the same manifest).
- The bidirectional-LLM-with-disabled-causal-mask + LoRA-adapter situation is the highest-risk part of this port. Stage 4 should bring up the encoder + projector + CTC head against the AR family's existing encoder code first, then bolt on the bidirectional LLM forward graph as a distinct step.