Merged self+cross attention (T5Gemma2 style): self-attention layer also projects encoder memory through a separate `memory_kv` linear and concatenates the result onto the self-attention K/V before softmax.
| 127 | // projects encoder memory through a separate `memory_kv` linear and concatenates |
| 128 | // the result onto the self-attention K/V before softmax. |
| 129 | TEST(MergedAttentionTest, ForwardMergedProducesOutput) { |
| 130 | constexpr dim_t NUM_HEADS = 4, NUM_KV = 1, D_HEAD = 16; |
| 131 | constexpr dim_t D_MODEL = NUM_HEADS * D_HEAD; |
| 132 | constexpr dim_t QKV_ROWS = (NUM_HEADS + 2 * NUM_KV) * D_HEAD; |
| 133 | constexpr dim_t KV_ROWS = 2 * NUM_KV * D_HEAD; |
| 134 | |
| 135 | class MergedMockModel : public models::Model { |
| 136 | public: |
| 137 | MergedMockModel() { |
| 138 | register_variable("attn/linear_0/weight", |
| 139 | StorageView({QKV_ROWS, D_MODEL}, std::vector<float>(QKV_ROWS * D_MODEL, 0.01f))); |
| 140 | register_variable("attn/linear_1/weight", |
| 141 | StorageView({D_MODEL, NUM_HEADS * D_HEAD}, |
| 142 | std::vector<float>(D_MODEL * NUM_HEADS * D_HEAD, 0.01f))); |
| 143 | register_variable("attn/memory_kv/weight", |
| 144 | StorageView({KV_ROWS, D_MODEL}, std::vector<float>(KV_ROWS * D_MODEL, 0.01f))); |
| 145 | register_variable("attn/q_norm/gamma", StorageView({D_HEAD}, std::vector<float>(D_HEAD, 1.0f))); |
| 146 | register_variable("attn/k_norm/gamma", StorageView({D_HEAD}, std::vector<float>(D_HEAD, 1.0f))); |
| 147 | register_variable("attn/num_heads_kv", StorageView(static_cast<int32_t>(NUM_KV))); |
| 148 | set_compute_type(ComputeType::FLOAT32, Device::CPU, 0, false); |
| 149 | } |
| 150 | protected: |
| 151 | std::unique_ptr<Model> clone() const override { return nullptr; } |
| 152 | }; |
| 153 | |
| 154 | MergedMockModel model; |
| 155 | layers::MultiHeadAttention attention(model, "attn", NUM_HEADS, /*self_attention=*/true); |
| 156 | ASSERT_TRUE(attention.has_merged_encoder_attention()); |
| 157 | |
| 158 | constexpr dim_t B = 1, Q_LEN = 1, MEM_LEN = 5; |
| 159 | StorageView queries({B, Q_LEN, D_MODEL}, std::vector<float>(B * Q_LEN * D_MODEL, 1.0f)); |
| 160 | StorageView memory({B, MEM_LEN, D_MODEL}, std::vector<float>(B * MEM_LEN * D_MODEL, 1.0f)); |
| 161 | StorageView output(DataType::FLOAT32); |
| 162 | StorageView self_k(DataType::FLOAT32), self_v(DataType::FLOAT32); |
| 163 | StorageView mem_k(DataType::FLOAT32), mem_v(DataType::FLOAT32); |
| 164 | |
| 165 | attention.forward_merged(queries, &memory, nullptr, nullptr, output, |
| 166 | &self_k, &self_v, &mem_k, &mem_v, nullptr, nullptr, /*offset=*/0); |
| 167 | |
| 168 | EXPECT_EQ(output.shape(), (Shape{B, Q_LEN, D_MODEL})); |
| 169 | EXPECT_EQ(mem_k.shape(), (Shape{B, NUM_HEADS, MEM_LEN, D_HEAD})); |
| 170 | EXPECT_EQ(self_k.shape(), (Shape{B, NUM_HEADS, Q_LEN, D_HEAD})); |
| 171 | } |
| 172 | |
| 173 | // MHA: Each head has independent K/V |
| 174 | TEST_F(CrossAttentionTest, StandardMultiHeadAttention) { |
nothing calls this directly
no test coverage detected