#pragma once #include "model_loader.hpp" #include "graph_builder.hpp" #include #include struct ggml_context; struct ggml_tensor; namespace pk { // A single FastConformer encoder layer (NeMo ConformerLayer), built per-layer // from the GGUF weights. Mirrors NeMo's ConformerLayer.forward exactly: // // r = x // r = r + 0.5 * feed_forward1(norm_feed_forward1(r)) # FFN1 (half-step) // r = r + self_attn(norm_self_att(r), pos_emb, mask) # MHSA (RelPosAttention) // r = r + conv(norm_conv(r)) # Conv module // r = r + 0.5 * feed_forward2(norm_feed_forward2(r)) # FFN2 (half-step) // out = norm_out(r) // // feed_forwardN = linear2(silu(linear1(x))). // conv (ConformerConvolution, operates on [d_model, T]): // pointwise_conv1 (d->2d, k=1) -> GLU(dim=channel) -> [zero padded time pos] // -> depthwise_conv (d->d, k=conv_kernel, groups=d) // -> norm -> SiLU -> pointwise_conv2 (d->d, k=1). // Two conv-module variants, selected by config (byte-identical gating): // * conv_norm_type=batch_norm (offline models): inference affine fold from // running stats (eps 1e-5); symmetric depthwise pad (k-1)/2 each side. // * conv_norm_type=layer_norm (streaming models, e.g. the EOU model): LayerNorm // over the channel dim per time-frame (eps 1e-5), gamma/beta read from // conv.batch_norm.{weight,bias} (NeMo always names the attr `batch_norm`). // conv_causal=true -> causal depthwise pad (left k-1, right 0). // All norm_* (outside conv) are LayerNorm (eps 1e-5). MHSA reuses pk::RelPosAttention. // // Layout convention (matches the rest of the port and the baseline GGUF): // x row-major [T, d_model] (d_model fastest) // pos_emb row-major [2T-1, d_model] (d_model fastest) // out row-major [T, d_model] (d_model fastest) // // `valid_len` is the number of non-padding frames (frames >= valid_len are // center-pad). It is threaded into RelPosAttention (key/query masking) and used // to zero padded time positions before the depthwise conv, matching NeMo's // pad_mask handling. Pass valid_len == T to disable masking. class ConformerLayer { public: ConformerLayer(const ModelLoader& ml, int layer_idx); // GRAPH-BUILDER: append the WHOLE conformer layer (FFN1 + MHSA + conv + FFN2 // + norm_out) to a SHARED graph `ctx`. `xt` is the layer input tensor [D, T] // and `pe` is the positional-encoding tensor [D, pos_len], both ALREADY in // the graph. Returns the layer output [D, T] (ggml ne0=D fastest, row-major // [T, d_model]). Host-built masks / batch-norm fold constants are fed via // pk::graph_input_tensor and registered into `pool` (must outlive compute). // This is the unit reused by the fused encoder AND the unit test; computing // the entire layer as ONE sub-graph (vs the old 5 sub-graphs) is what lets // the fused encoder be a single graph. // When att_left/att_right >= 0, the self-attention uses NeMo // rel_pos_local_attn (banded, O(T*window)): `pe` must then be the LOCAL // positional encoding [d_model, att_left+att_right+1]. Defaults (-1, -1) keep // full attention with `pe` = [d_model, 2T-1]. ggml_tensor* build_graph(ggml_context* ctx, ggml_tensor* xt, int T, ggml_tensor* pe, int pos_len, int valid_len, GraphInputPool& pool, int att_left = -1, int att_right = -1) const; // Batched GRAPH-BUILDER. `xt` is [D, T, B]; `pe` is [D, pos_len] (shared // across the batch). `valid_len` is per item (size B). Returns [D, T, B]. // att_left/att_right >= 0 routes self-attention to banded local attention // (pe = LOCAL [d_model, att_left+att_right+1]); defaults (-1,-1) = full. ggml_tensor* build_graph_batched(ggml_context* ctx, ggml_tensor* xt, int T, int B, ggml_tensor* pe, int pos_len, const std::vector& valid_len, GraphInputPool& pool, int att_left = -1, int att_right = -1) const; // x: [T, d_model]; pos_emb: [pos_len=2T-1, d_model]; out: [T, d_model]. void forward(const std::vector& x, int T, const std::vector& pos_emb, int pos_len, int valid_len, std::vector& out) const; // Same as forward(), but also returns the ConformerConvolution sub-module // output (NeMo `layers[i].conv` output, row-major [T, d_model]) for parity // localization against the baseline `l0_conv_out`. void forward_with_conv(const std::vector& x, int T, const std::vector& pos_emb, int pos_len, int valid_len, std::vector& out, std::vector& conv_out) const; // Run JUST the ConformerConvolution sub-module (everything AFTER norm_conv: // pointwise_conv1 -> GLU -> [pad_mask] -> depthwise_conv -> norm -> SiLU -> // pointwise_conv2) on `conv_in` (= norm_conv(residual), row-major [T,d_model]). // Test entry point for validating the conv module — including the layer_norm // and causal-conv variants — in ISOLATION from the surrounding attention. // `out` is row-major [T, d_model]. void conv_module_forward(const std::vector& conv_in, int T, int valid_len, std::vector& out) const; private: const ModelLoader& ml_; int layer_idx_; int d_model_; int n_heads_; int ff_dim_; int conv_kernel_; std::string conv_norm_type_; // "batch_norm" (offline) or "layer_norm" (streaming) bool conv_causal_ = false; // causal depthwise conv pad (left k-1, right 0) }; } // namespace pk