Discord

Checklist NOAI Singapore 2026 Final Assessment · Task 3

SOTA-AI Editorial

The Attention Mechanism (Building the "Brain")

Implement scaled dot-product attention, multi-head attention, a corrected positional encoding and a classification head in PyTorch.

Written
8 October 2026
Result
Synthetic CPU checks passed; no task-level score, training run, or organizer grading result is available.
Points
No points score reported; see result for the measured metric.
Olympiad
National Olympiad in Artificial Intelligence (NOAI), Singapore – organised by AI Singapore with NTU, 2026 · Final Assessment

Hints

Hint 1

Compute `QKᵀ / sqrt(d_k)`, mask excluded keys before softmax, then multiply the weights by `V`.

Hint 2

Project Q/K/V into `num_heads`, transpose to put heads before sequence positions, and reverse the reshape after attention.

Hint 3

Add sinusoidal positions before the encoder stack and mean-pool the sequence outputs for classification.

Hint 4

Use the same frequency scale for even sine and odd cosine columns, and check the final cosine slice for odd `d_model`.

The solution in one line

Build the Transformer pieces explicitly, preserve the head and sequence dimensions, and apply finite-score masks before softmax.

The editorial

NOAI Singapore 2026 — The Attention Mechanism (Building the “Brain”)

Problem domain: NLP, Transformers, deep learning

Evaluation metric: 35 marks total: 10 for scaled dot-product attention, 10 for multi-head attention, 5 for positional encoding, 5 for the classifier, and a 5-mark causal-mask bonus. There is no released text-classification score or public grader result.

Abridged statement

Implement a Transformer encoder's building blocks in PyTorch: scaled dot-product attention with optional masking, multi-head attention, sinusoidal positional encoding, an encoder layer and a small classification head. Implement a lower-triangular causal mask for the bonus. The notebook also has a short written question about why attention scores are scaled.

Data analysis

The task releases no text corpus, labels, pretrained checkpoint, or accuracy benchmark. Its checks use synthetic tensors. The attention example uses query, key, and value arrays shaped (2, 8, 10, 64). The multi-head example uses a (2, 10, 512) input with eight heads, while the classifier example draws token IDs from a 30,000-token vocabulary and checks for two output logits per sample. The notebook masks the final three keys in the attention check and the final two tokens in the classifier check. These checks assess tensor shapes and mask behavior, not a trained model's predictive quality.

Experiments

The recorded local PyTorch checks passed for attention output and weight shapes, row-normalized weights, masking of future positions, multi-head output shape, positional encoding with an odd feature width, classifier output shape, and the lower-triangular causal mask. No accuracy or numeric mark total can be computed from synthetic inputs. The official notebook examples were inspected, not executed as a reference run.

Solution

The implementation projects queries, keys, and values, splits them into heads, computes softmax(QKᵀ / sqrt(d_k)), applies the weights to values, joins heads, and applies the output projection. It implements residual attention and feed-forward blocks, sinusoidal positions, and a mean-pooled classification head. The position encoding uses the standard exponential frequency formula and safely handles odd d_model values. The causal mask is a broadcastable lower-triangular keep-mask.

Score evidence and limits

No mark score was measured. The 35 marks are the official maximum, not a measured result. The recorded evidence consists of local shape and mask assertions; there is no released grader result, labeled evaluation corpus, or validation accuracy. The task notebook has two internal inconsistencies: its positional-encoding section says a formula is broken, but the actual div_term assignment in that notebook already matches the standard formula; and the answer-cell label says “10 pts” even though Part 3 assigns 5 marks. The solution follows the standard equation and does not claim a score for either disputed cell.

Implementation and diagnostics

For attention scores, the implementation divides by the square root of the head width, applies a broadcastable keep-mask before softmax, and uses a large finite negative value for masked scores. Multi-head attention reshapes and transposes the sequence and head axes before reversing that transformation after attention. Positional encoding alternates sine on even columns and cosine on odd columns; it slices the frequency vector correctly when the embedding width is odd. The classifier mean-pools over the sequence dimension and returns class logits.

Rules and dependencies: The task asks for the components to be implemented in PyTorch and explicitly says masked logits should use a large finite negative value such as -1e9, not -inf. The solution uses PyTorch modules/functions plus Python's math; it does not substitute a built-in MultiheadAttention layer for the assessed component.

Compute and footprint

The checks used synthetic tensors on CPU. No runtime benchmark, training run, GPU result, pretrained weights, or external data was recorded or required.

Alternatives considered

  • PyTorch's built-in MultiheadAttention would conceal the projections, head reshaping, and scaled-attention operation the exercise asks to implement.
  • A boolean keep-mask and an additive mask are both common. This implementation uses a broadcastable keep-mask and fills excluded scores with a finite negative value before softmax.
  • A fixed even-width-only position encoder is shorter, but the general implementation supports odd embedding widths without a cosine-slice shape error.

Sources and reuse

The source does not state a reuse license. This report paraphrases the task and the solution code is independently written; the notebooks are linked rather than copied into this repository.

Code

Solution code solution.py · 126 lines
"""Reference implementation for NOAI Singapore 2026 Programming Task 3."""

from __future__ import annotations

import math

import torch
from torch import nn
from torch.nn import functional as F


def scaled_dot_product_attention(query, key, value, mask=None, dropout=None):
    """Compute scaled dot-product attention and return output and weights.

    Expected inputs have shape (batch, heads, query_length, key_dimension),
    with values ending in value_dimension. A mask is broadcast over score
    dimensions; nonzero/True entries are kept.
    """
    d_k = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None:
        # Keep the fill finite in every floating-point dtype, including fp16.
        neg = max(-1e9, torch.finfo(scores.dtype).min)
        scores = scores.masked_fill(mask == 0, neg)
    attention_weights = F.softmax(scores, dim=-1)
    if dropout is not None:
        attention_weights = dropout(attention_weights)
    output = torch.matmul(attention_weights, value)
    return output, attention_weights


class MultiHeadAttention(nn.Module):
    """Project Q/K/V, apply parallel attention heads, then combine them."""

    def __init__(self, d_model: int, num_heads: int, dropout: float = 0.1) -> None:
        super().__init__()
        if d_model % num_heads:
            raise ValueError("d_model must be divisible by num_heads")
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)
        q = self.W_q(query).reshape(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        k = self.W_k(key).reshape(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        v = self.W_v(value).reshape(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        attended, weights = scaled_dot_product_attention(
            q, k, v, mask=mask, dropout=self.dropout
        )
        attended = attended.transpose(1, 2).contiguous().reshape(
            batch_size, -1, self.d_model
        )
        return self.W_o(attended), weights


class PositionalEncoding(nn.Module):
    """Sinusoidal positional encoding with even-sine / odd-cosine columns."""

    def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.1) -> None:
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(max_len, dtype=torch.float32).unsqueeze(1)
        div_term = torch.exp(
            torch.arange(0, d_model, 2, dtype=torch.float32)
            * (-(math.log(10000.0) / d_model))
        )
        pe[:, 0::2] = torch.sin(position * div_term)
        # For odd d_model the odd-column slice has one fewer column.
        pe[:, 1::2] = torch.cos(position * div_term[: pe[:, 1::2].shape[1]])
        self.register_buffer("pe", pe.unsqueeze(0))

    def forward(self, x):
        if x.size(1) > self.pe.size(1):
            raise ValueError("sequence length exceeds configured max_len")
        return self.dropout(x + self.pe[:, : x.size(1)].to(dtype=x.dtype))


class TransformerEncoderLayer(nn.Module):
    """Single self-attention and feed-forward encoder block."""

    def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout: float = 0.1):
        super().__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads, dropout)
        self.feed_forward = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model),
        )
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        attn_out, _ = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_out))
        ff_out = self.feed_forward(x)
        return self.norm2(x + self.dropout(ff_out))


class TransformerClassifier(nn.Module):
    """Encoder classifier with mean pooling, matching the task instructions."""

    def __init__(
        self,
        vocab_size: int,
        d_model: int = 256,
        num_heads: int = 8,
        num_layers: int = 4,
        d_ff: int = 1024,
        num_classes: int = 2,
        max_len: int = 512,
        dropout: float = 0.1,
    ) -> None:
        super().__init__()
        self.d_model = d_model
        self.token_embedding = nn.Embedding(vocab_size, d_model)
        self.pos_encoding = PositionalEncoding(d_model, max_len, dropout)
        self.encoder_layers = nn.ModuleList(
            TransformerEncoderLayer(d_model, num_heads, d_ff, dropout)
            for _ in range(num_layers)
        )
        self.classifier = nn.Sequential(
            nn.LayerNorm(d_model),
            nn.Linear(d_model, d_model),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(d_model, num_classes),
        )

    def forward(self, input_ids, attention_mask=None):
        x = self.token_embedding(input_ids) * math.sqrt(self.d_model)
        x = self.pos_encoding(x)
        if attention_mask is not None:
            attention_mask = attention_mask.unsqueeze(1).unsqueeze(2)
        for layer in self.encoder_layers:
            x = layer(x, attention_mask)
        return self.classifier(x.mean(dim=1))


def create_causal_mask(seq_len: int, device="cpu") -> torch.Tensor:
    """Return a broadcastable lower-triangular keep-mask (1, 1, T, T)."""
    return torch.tril(torch.ones(seq_len, seq_len, device=device)).unsqueeze(0).unsqueeze(0)

Download solution.py

Editorials are written by members of the SOTA community, not by the olympiad. The official statement and scoring win wherever an editorial differs.