Skip to content

Latest commit

 

History

History
98 lines (79 loc) · 12.4 KB

File metadata and controls

98 lines (79 loc) · 12.4 KB

Forward map - medasr

Reference: transformers.models.lasr.modeling_lasr @ 65dc261512cbdb1ee72b88ae5b222f2605aad8e5 (v5.0.0.dev0) Closest in-tree analog: src/arch/parakeet/ (Conformer + macaron) + src/causal_lm/ (RoPE) + src/arch/sensevoice/ (CTC head)

MedASR is the first in-tree encoder-ctc Conformer family. It does NOT fit build_conformer_block from src/conformer/conformer.h because the upstream block uses load-bearing scaled residuals on every sub-block (macaron [1.5, 0.5], conv [2.0, 1.0]) rather than the standard 0.5 macaron half-step, and the attention path is RoPE rather than relative-position. The block is therefore hand-built in src/arch/medasr/encoder.cpp using the lower-level shared helpers (layer_norm, feed_forward, conv_1d_dw_f32, fused_batch_norm, add_conv_bias) plus a per-family rope_mhsa written from src/causal_lm/'s ggml_rope_ext pattern.

Frontend

Stage Reference location Output shape Gate tensor ggml / C++ pattern In-tree analog
log-mel feature_extraction_lasr.LasrFeatureExtractor.__call__ [T_mel=floor((n_pcm-400)/160)+1, 128] f32 mel.in host-side transcribe::MelFrontend; LASR knobs require center=false, pad_mode="zero", pre_emphasis=0.0, normalize="none", window=ckpt, filterbank=ckpt, log_compression="log_clamp_1e-5". Step 2 brings up the encoder with TRANSCRIBE_MEL_FROM_REF=<ref_dir> so Step 7 (frontend parity) is the first place the C++ mel actually runs. src/transcribe-mel.{h,cpp} (extend with center=false knob and log_compression enum)

For jfk.wav (176 400 samples): T_mel = (176 400 − 400)/160 + 1 = 1 099 frames — reference dump rounds to 1 098 (one trailing frame dropped because LASR's manual unfold does len // win floor on the residual; not centered). The forward-map records the formula; bit-exact frame count is a Step 7 parity check.

Encoder

Input: mel ne [T_mel, 128, 1, B] f32 (matches MelFrontend row-major output).

Stage Reference location Output shape Gate tensor ggml / C++ pattern In-tree analog
Subsampling: dense_0 LasrEncoderSubsampling.dense_0 [T_mel, 512] mul_mat + bias add parakeet feed_forward w/o second linear
Subsampling: relu0 F.relu [T_mel, 512] ggml_relu n/a
Subsampling: conv_0 Conv1d(512, 512, k=5, s=2, p=0) [T1=floor((T_mel-5)/2)+1, 512] conv_1d_f32 (kernel [KW=5, IC=512, OC=512], stride 2, pad 0) conformer conv_1d_f32
Subsampling: conv_0 bias + relu bias add + F.relu [T1, 512] add_conv_bias + ggml_relu conformer
Subsampling: conv_1 Conv1d(512, 256, k=5, s=2, p=0) [T_enc=floor((T1-5)/2)+1, 256] conv_1d_f32 conformer
Subsampling: conv_1 bias + relu bias + F.relu [T_enc, 256] add_conv_bias + ggml_relu n/a
Subsampling: dense_1 Linear(256, 512) + bias [T_enc, 512] enc.subsampling.out mul_mat + bias add n/a
Block ×17: norm_ff1 LayerNorm(512, eps=1e-6, bias=False) [T_enc, 512] (in post_ff1) layer_norm(x, w, nullptr) with eps=1e-6 conformer layer_norm (eps hardcoded 1e-5 — see deviation)
Block ×17: ff1 LasrFeedForward = Linear → SiLU → Linear (no biases) [T_enc, 512] feed_forward(x, lin1_w, nullptr, lin2_w, nullptr) conformer feed_forward
Block ×17: scaled-residual ff1 x = x*1.5 + ff1*0.5 [T_enc, 512] enc.block.0.post_ff1 (block 0 only) ggml_add(ggml_scale(x, 1.5), ggml_scale(ff1, 0.5)) NOT macaron_ff_residual (deviation)
Block ×17: norm_attn LayerNorm no bias layer_norm w/ eps 1e-6 conformer
Block ×17: self-attn Q/K/V Linear no bias each → reshape [d_model] → [head_dim, n_head] [head_dim=64, n_head=8, T_enc, B] three mul_mat then reshape_3d+permute for head layout causal_lm Q/K/V projection
Block ×17: RoPE on Q, K LasrEncoderRotaryEmbedding (rope_theta=10 000, type=default) applied per-head unchanged ggml_rope_ext(Q, positions, nullptr, head_dim, GGML_ROPE_TYPE_NEOX, max_pos=10000, rope_theta=10000, 1,0,1,32,1) and same for K causal_lm RoPE call site (deviation: no MRoPE, no extrapolation)
Block ×17: scaled-dot attn softmax(QK/√d_k)·V, no mask [head_dim, n_head, T_enc, B] flash_attn_ext or manual mul_mat+soft_max causal_lm
Block ×17: o_proj Linear no bias [T_enc, 512] reshape back + mul_mat causal_lm
Block ×17: residual attn x = x + attn (UNSCALED) [T_enc, 512] enc.block.0.post_attn ggml_add standard
Block ×17: norm_conv LayerNorm no bias layer_norm conformer
Block ×17: conv module Conv1d pw1(512→1024,k=1) → GLU → Conv1d dw(512,k=32,p=15,g=512)BatchNorm1d(512)SiLUConv1d pw2(512→512,k=1) [T_enc, 512] mul_mat + ggml_swiglu + conv_1d_dw_f32 + fused_batch_norm + silu + mul_mat. Depthwise uses non-centered pad=15 (left=15, right=16 to keep T identical with k=32 even kernel) — verify against reference. conformer conv_module (cannot reuse — that helper bakes in the unscaled x + conv residual; we need a custom scaled residual outside)
Block ×17: scaled-residual conv x = x*2.0 + conv*1.0 [T_enc, 512] enc.block.0.post_conv ggml_add(ggml_scale(x, 2.0), conv) deviation
Block ×17: norm_ff2 + ff2 same shape as FF1 [T_enc, 512] mirrors FF1 conformer
Block ×17: scaled-residual ff2 x = x*1.5 + ff2*0.5 [T_enc, 512] enc.block.0.post_ff2 ggml_add(ggml_scale(x, 1.5), ggml_scale(ff2, 0.5)) deviation
Block ×17: norm_out LayerNorm no bias [T_enc, 512] enc.block.{0,7,8,16}.out layer_norm w/ eps 1e-6 conformer
enc.out_norm top-level LayerNorm no bias [T_enc, 512] enc.out_norm.out layer_norm conformer

Decoder

n/a — non-autoregressive CTC head.

Stage Reference location Output shape Gate tensor ggml / C++ pattern In-tree analog
CTC head LasrCtcHead.ctc_head = Conv1d(512, 512, k=1) + bias [vocab=512, T_enc, 1, B] (note [vocab, T] order) enc.ctc_logits ggml_mul_mat(W_ctc, x) + b_ctc (k=1 conv = mul_mat) sensevoice ctc_head
Greedy decode argmax + collapse-repeats + drop-blanks (blank=0) [N_tokens] host ints host-side loop on logits adapt parakeet decode_ctc_greedy (blank=last-id → blank=0)
Detokenize LasrTokenizer.batch_decode (SP) — keeps </s> id 2 in stream string transcript.json shared Tokenizer::decode_ctc strips blanks pre-call shared

Generation / KV Path

n/a — encoder-only CTC; no autoregressive state. run_batch fast path batches utterances via the existing batch-axis (ne[3]) of the encoder graph, identical to parakeet's CTC variant.

Capabilities And Language Controls

Capability Reference behavior C++ API behavior Family-doc Capability Validation row
English transcribe generate greedy CTC same "Transcribe / explicit en" + "Transcribe / auto" — both MUST PASS
Language detection n/a (English-only) reject --language != en with OK fallback to en SKIP — not advertised
Translation n/a not exposed SKIP — not advertised
Timestamps n/a (CTC has none) not exposed SKIP — architecture has no timestamp head
Streaming n/a (offline only) not exposed SKIP — capabilities.streaming=false
Batch (offline) HF processor pads to common T_mel implement run_batch via ne[3] batch axis on encoder graph (mirror parakeet CTC pattern); per-utterance valid-frame masks via attn_pad_mask / conv_pad_mask on each block MUST PASS — text byte-identical, CPU tensor bit-exact at bs 2/4/8
KenLM beam decode shipped but not Stage-4 obligation not exposed SKIP — OUT OF SCOPE (first ship is greedy)

Deviations From Closest Analog

  • Macaron FF residual scalars are [1.5, 0.5], NOT the standard 0.5. The shared transcribe::conformer::macaron_ff_residual helper hardcodes x + 0.5 * FF(LN(x)) and is not reusable here. medasr hand-builds the FF1/FF2 residual as ggml_add(ggml_scale(x, 1.5), ggml_scale(FF(LN(x)), 0.5)).
  • Conv module residual scalars are [2.0, 1.0]. transcribe::conformer::conv_module itself is callable for the inner pw1+GLU+dw+BN+SiLU+pw2 sub-block, but the surrounding scaled residual is hand-built (ggml_add(ggml_scale(x, 2.0), conv)).
  • Attention is RoPE, not relative-position. transcribe::conformer::rel_pos_mhsa and build_conformer_block are not used. The Q/K projection + ggml_rope_ext(..., GGML_ROPE_TYPE_NEOX, ...) + softmax(QK/√d)·V + o_proj is implemented per-block in src/arch/medasr/encoder.cpp using the causal_lm pattern at src/causal_lm/causal_lm.cpp:234-245. rope_theta=10 000, max_position_embeddings=10 000.
  • layer_norm eps is 1e-6, not the conformer-helper-hardcoded 1e-5. The shared helper uses ggml_norm with constant kLayerNormEps = 1e-5f. medasr loads enc.layer_norm_eps from the GGUF (stt.medasr.encoder.layer_norm_eps) and passes it through a local medasr_layer_norm shim that takes eps as a runtime argument.
  • All encoder LayerNorms are bias=False. Every _b slot on the medasr block view stays nullptr; layer_norm is called with beta=nullptr. Same null-bias treatment as parakeet's FFN/attn linears; just applied to LN this time.
  • Subsampling is 1-D, not 2-D. LasrEncoderSubsampling = Linear(128→512) → ReLU → Conv1d(512,512,k=5,s=2) → ReLU → Conv1d(512,256,k=5,s=2) → ReLU → Linear(256→512). The leading Linear(128→512) reduces the mel dim to d_model BEFORE the convs, so the convs operate on [T, 512] channels (not [F=128, T] 2-D as in parakeet's DwStridingSubsampling). Two stride-2 convs → 4x effective downsampling. transcribe::conformer::build_pre_encode is not reusable; medasr writes its own build_subsampling in encoder.cpp using conv_1d_f32.
  • CTC head is Conv1d(512→512, k=1) with bias, not Linear no-bias. Numerically equivalent to a linear projection but stored as Conv1d. The converter normalizes the tensor name to ctc.proj.weight (transposed if necessary to mul_mat-friendly layout); C++ runs ggml_add(ggml_mul_mat(W, x), b).
  • CTC blank id = 0 (<epsilon>), NOT vocab_size - 1. Parakeet/sensevoice convention is blank = vocab_size - 1. medasr reads stt.medasr.ctc.blank_id from the GGUF and uses it in greedy collapse. Hard-coded last_id assumptions in the shared CTC decoder are NOT reused — medasr ships its own greedy collapse in decode_ctc.cpp (or inlined in model.cpp::run).
  • Tokenizer specials match SentencePiece ids 0..3 (<epsilon>, <s>, </s>, <unk>). The reference WER scorer (scripts/wer/run_reference_medasr_transformers.py) uses processor.batch_decode(token_ids, skip_special_tokens=True). The validate.py reference dumper does the same (changed from the README's non-skipping variant, see commit), so both reference paths now agree. The C++ greedy CTC collapse mirrors this by skipping ids 1 (<s>), 2 (</s>), and 3 (<unk>) in addition to the blank id 0. Without this skip, the CTC head's occasional last-frame </s> prediction leaks the literal string into the transcript and lifts dataset WER by ~0.2pp on LibriSpeech test-clean.
  • Conv-module depthwise dispatch is direct_dw_in_block=true (NOT the im2col path). LASR's conv_kernel=32 "same" padding is asymmetric (15, 16); the conformer-helper's asymmetric-pad path uses ggml_fill + ggml_concat per side, which is NOT batch-stable in ggml CPU (drift ~1.4e-6 at the encoder output between B=1 and B>1). The direct depthwise op (ggml_conv_2d_dw_direct) handles the same kernel + symmetric inner-padding shape and is bit-exact across batch sizes. The trade-off is a Metal note: direct depthwise on k=32 has not been profiled on Apple GPU and may need a fallback there; CPU + Vulkan + CUDA are fine.

Variant Notes

  • medasr: single variant; no per-variant deviations. The variants list in intake.json has length 1. config.varying_across_variants is empty.