Compute the output of a single-head cross-attention layer given explicit query, key and value matrices - the operation that lets learned queries (or text tokens) read from image features.
Cross-attention is the same computation as self-attention except that the queries come from one stream and the keys/values from another. In a VLM the queries are language-side (or the Q-Former's learned queries) and the keys/values are the vision encoder's patch features:
Attention(Q,K,V)=softmax(dkQK⊤)V
with Q of shape (n_q, d_k), K of shape (n_kv, d_k), V of shape (n_kv, d_v), and the output of shape (n_q, d_v). The output length follows the queries, not the keys - which is exactly why a resampler with 64 queries turns 576 patch features into 64 tokens.
Two things trip people up:
Implement:
def cross_attention(Q, K, V):
Return the (n_q, d_v) output as a nested list with every entry rounded to 4 decimals.
A nested list of n_q rows of d_v floats, each rounded to 4 decimals.
Q = [[1.0, 0.0]]
K = [[1.0, 0.0], [0.0, 1.0]]
V = [[1.0, 2.0], [3.0, 4.0]]
print(cross_attention(Q, K, V))
Output:
[[1.6605, 2.6605]]
Q = [[1.0, 0.0]] K = [[1.0, 0.0], [0.0, 1.0]] V = [[1.0, 2.0], [3.0, 4.0]] print(cross_attention(Q, K, V))
[[1.6605, 2.6605]]
Scores are [1, 0] divided by sqrt(2) = [0.7071, 0]. Softmax gives weights [0.6698, 0.3302], so the mix of the two value rows is 0.6698*[1,2] + 0.3302*[3,4] = [1.6605, 2.6605]. Skipping the sqrt(2) scale would give weights [0.7311, 0.2689] and the different answer [1.5379, 2.5379].
1 <= n_q, n_kv <= 64, 1 <= d_k, d_v <= 641 / sqrt(d_k) where d_k is the QUERY/KEY width, not d_v-0.0