Token Embedding¶
The embedding operator maps integer token ids to their hidden-state rows — the first layer of the Qwen3/Llama stack. It is a WS1 ground-truth reference (issue #108): a pure-PyTorch definition of the "correct answer" that downstream fused CUDA/Triton kernels are validated against.
- Embedding (
NativeEmbeddingOp):out = weight[token_ids]— a plain row gather.
For Qwen3-8B the table is the input embedding [vocab=151936, hidden=4096] and is
independent from the lm_head weight (tie_word_embeddings=false) — the two weights
are not shared.
Entry Point¶
from rl_engine.kernels.registry import kernel_registry
embedding = kernel_registry.get_op("embedding")
h = embedding(token_ids, weight) # [B, S], [vocab, hidden] -> [B, S, hidden]
The op exposes the WS1 dual-path contract:
forward(...)— gathers in the weight's native dtype, casts the gathered rows back to the weight dtype (Axis-B accuracy candidate / dtype-behavior path).forward_fp32(...)— native-dtype gather, then upcasts the result to fp32 (the ground-truth golden path).
Backends¶
| Backend | Wrapper | Native symbol | Status |
|---|---|---|---|
| PyTorch fallback | NativeEmbeddingOp |
None | fp32 ground-truth reference; CPU and any GPU. |
| CUDA / ROCm / Triton | — | — | Planned: downstream fused kernels validate against this reference. |
Tensor Contract¶
| Argument | Shape | Dtype | Requirements |
|---|---|---|---|
token_ids |
[B, S] (any shape) |
integer | Index dtype; cast to int64 internally. Values in [0, vocab). |
weight |
[vocab, hidden] |
float (fp16/bf16/fp32) | Embedding table (Qwen3-8B [151936, 4096]). |
| output | token_ids.shape + (hidden,) |
forward: weight dtype · forward_fp32: float32 |
Gathered rows. |
Output dtype follows weight (the float operand); token_ids stay integer. Pure
function — no randomness, no in-place mutation, device/dtype follow the inputs.
Dispatch Behavior¶
kernel_registry.get_op("embedding") resolves through the OpBackend priority map. On
cuda / rocm / cpu the only registered backend today is the PyTorch native op
(PYTORCH_NATIVE_EMBEDDING), so every device dispatches to the fp32 reference. When fused
kernels land, they are prepended to the priority list and the native op becomes the fallback.
Accuracy¶
Reference semantics (forward_fp32):
- Ground truth:
forward_fp32gathers in the native dtype, then upcasts to fp32. Because a gather is a lossless row copy, this is bitwise-identical to upcasting the whole table first — but it never allocates a multi-GB fp32 copy of the full vocab table for a tiny lookup; only the gathered rows are upcast. - Dtype path:
forwardruns the same gather, then casts back to the weight dtype; it is bitwise-equal toforward_fp32(...).to(dtype). - Lossless gather — no accuracy drift: a row gather performs no reduction and no floating-point accumulation, so the result is bit-exact at every dtype. There is no Axis-B tolerance to calibrate; the gathered rows equal direct indexing exactly.
- Axis A — batch invariance: each token's row is independent, so the output is
bitwise-identical regardless of batch size or padding (
torch.equal,atol=0).
Performance Notes¶
Reference operator — no fused kernel or benchmark yet. Downstream fused kernels carry their own benchmarks and are measured against this reference for correctness.
Tests¶
Covers: correctness vs direct indexing (bitwise), dtype paths, non-int64 id tolerance,
Axis-A batch invariance (slice + padding), input purity, gradient flow to weight
(including sparse-grad: unused rows stay zero), registry dispatch, and a GPU-only smoke
test at the real Qwen3-8B dims (vocab=151936, hidden=4096, boundary ids 0 and
vocab-1) that skips when CUDA or GPU memory is unavailable.
Implementation Files¶
rl_engine/kernels/ops/pytorch/linear/embedding.pyrl_engine/kernels/registry.pytests/test_embedding.py
Known Limitations¶
- PyTorch fallback only; no fused CUDA/Triton backend yet (downstream work).
- Out-of-range token ids are not validated; callers must keep ids in
[0, vocab). - GPU backward is bitwise-reproducible only under deterministic algorithms. The
forward is a lossless gather (always reproducible), but
∂L/∂weightis a scatter-add: every repeated token id (padding, common tokens) accumulates into the sameweight.gradrow. On CUDA that accumulation uses atomic adds, whose ordering is nondeterministic, so the weight gradient is not bit-exact across runs when ids collide. PyTorch documentsembeddingbackward as a nondeterministic CUDA op for this reason. Sinceforward_fp32is the backward golden source, callers that need a reproducible GPU gradient must enabletorch.use_deterministic_algorithms(True)(the gradient test does this). CPU backward is always deterministic.