18 · Content revision 7
Give token IDs trainable vectors
Build a trainable token table, validate public token IDs once, pass owned selectors through a private validated gather plan, and scatter-add repeated-token gradients.
Predict three selected rows and one shared gradient
Use this trainable table, whose four vocabulary rows each have width :
Before running anything, read the IDs from left to right. The first and third positions both select row ; the middle position selects row . The predicted output is therefore , with shape . Repetition copies a value into two positions, but it does not create a second trainable row.
One-hot notation makes the selection visible. For example, ID corresponds to . Multiplying that indicator by leaves . The runnable contrast materializes those zeros only for this tiny explanation:
rust/demos/ch18-token-embeddings/src/lib.rs#one-hot-baseline /// Materializes the tiny historical algebraic baseline for comparison only.
pub fn explicit_one_hot_product(table: &Tensor, token_ids: &[u32]) -> (Vec<Vec<u8>>, Vec<f64>) {
assert_eq!(table.shape().len(), 2);
let vocabulary_size = table.shape()[0];
let width = table.shape()[1];
let mut indicators = Vec::with_capacity(token_ids.len());
let mut output = Vec::with_capacity(token_ids.len() * width);
for &token_id in token_ids {
let selected = usize::try_from(token_id).expect("u32 token ID must fit usize");
assert!(selected < vocabulary_size);
let mut one_hot = vec![0_u8; vocabulary_size];
one_hot[selected] = 1;
for feature in 0..width {
let value = one_hot
.iter()
.enumerate()
.map(|(row, &active)| f64::from(active) * table.as_slice()[row * width + feature])
.sum();
output.push(value);
}
indicators.push(one_hot);
}
(indicators, output)
} The actual layer selects the row directly and verifies that both routes produce the same exact values:
rust/demos/ch18-token-embeddings/src/lib.rs#known-token-lookup let embedding = known_embedding();
let output = embedding.forward(&TOKEN_IDS, &TOKEN_SHAPE)?;
let (_, one_hot_output) = explicit_one_hot_product(&known_table(), &TOKEN_IDS);
let one_hot_matches = output.value().as_slice() == one_hot_output; Select forward, accumulate backward
The complete lookup and reverse rule is:
The forward half copies one selected row into each output position. Let be the scalar loss. An overbar is reverse-mode shorthand: is the gradient arriving at an output position, while is the gradient of table row . The reverse half follows the forward selections in the opposite direction. If several positions selected the same row, their upstream vectors add feature by feature into that one shared parameter row.
For the declared seed , row receives and row receives . Unused rows and receive zero. Token IDs receive no gradient because they are discrete selectors, not tape operands.
Keep vocabulary axes separate from feature axes
- is the trainable token table with shape .
- is the vocabulary size and therefore the number of rows in .
- is the embedding width and therefore the number of features in each row.
- is the integer token ID at batch index and sequence position .
- identifies one batch item; identifies one position in its sequence.
- means every coordinate on the final feature axis.
- is the selected width- vector at position .
- is the upstream gradient vector arriving at that output position.
- is the table-row gradient after contributions from every matching position have been accumulated.
- is one vocabulary-row index.
- The sum visits every whose equals .
The numeric distance between IDs is not a semantic distance. IDs and are adjacent only in the tokenizer’s numbering; training determines whether their learned vectors become similar.
From sparse identity to the vector entrance of a Transformer
A sparse one-hot word representation assigns one coordinate to each vocabulary item but expresses no graded similarity between words; explicitly carrying that vocabulary-wide vector also wastes work when only one row is needed.
Bengio et al., A Neural Probabilistic Language Model: Bengio et al. represent the mapping from a vocabulary word index to distributed features as a trainable matrix with one row per vocabulary item and one column per learned feature, share it across context positions, and learn it jointly with next-word prediction.
Bengio et al. learn a shared dense word-feature table jointly with a neural next-word model. The Transformer retains learned token embeddings for subword tokens, then adds positional information before its stacked attention and feed-forward computations.
Vaswani et al., Attention Is All You Need: Vaswani et al. use learned embeddings whose width matches the model width for BPE or word-piece tokens and add positional encodings before the Transformer stack; their embedding forward scaling is separate from parameter initialization.
The decoder’s token IDs enter the numeric model by selecting rows from one trainable vocabulary-by-feature table. Repeated IDs share the same parameter row, so their reverse contributions add; positional information, embedding forward scaling, attention, and output-weight tying remain later concerns.
One-hot vectors make token identity explicit but carry a vocabulary-sized field of zeros. Learned dense word features let neural language models share statistical strength, and Transformers keep learned token embeddings as the numeric entrance to deeper sequence computation. The algebraic one-hot identity explains direct row lookup, while the shared trainable row explains why repeated-token gradients add.
The essential progression is from sparse identity codes to learned vectors that are shared wherever the same token occurs. A concrete implementation may choose different integer types, storage layouts, initializers, and error conventions without changing the lookup or reverse-mode equations. The one-hot product is an explanatory identity, not a claim that either paper stores those sparse vectors.
Validate token IDs once, then reuse the gather rule
The layer reports table, token-shape, count, bounds, conversion-allocation, and delegated autodiff failures without partial output:
rust/crates/llm-from-scratch/src/nn/embedding.rs#embedding-errors /// A rejected embedding table, token layout, selector, or delegated operation.
#[derive(Clone, Debug, PartialEq)]
pub enum EmbeddingError {
Initialization(InitializationError),
Autodiff(TensorAutodiffError),
TableRank {
rank: usize,
},
EmptyVocabulary,
ZeroEmbeddingWidth,
TokenShape(TensorError),
TokenCountMismatch {
expected: usize,
actual: usize,
},
TokenIdOutOfBounds {
position: usize,
id: u32,
vocabulary_size: usize,
},
IndexAllocationFailed {
elements: usize,
},
}
impl fmt::Display for EmbeddingError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Initialization(error) => error.fmt(formatter),
Self::Autodiff(error) => error.fmt(formatter),
Self::TableRank { rank } => {
write!(
formatter,
"embedding table must have rank two, got rank {rank}"
)
}
Self::EmptyVocabulary => {
formatter.write_str("embedding vocabulary must contain at least one row")
}
Self::ZeroEmbeddingWidth => {
formatter.write_str("embedding width must be greater than zero")
}
Self::TokenShape(error) => write!(formatter, "invalid token-ID shape: {error}"),
Self::TokenCountMismatch { expected, actual } => write!(
formatter,
"token-ID shape needs {expected} IDs, but received {actual}"
),
Self::TokenIdOutOfBounds {
position,
id,
vocabulary_size,
} => write!(
formatter,
"token ID {id} at flat position {position} is out of bounds for vocabulary size {vocabulary_size}"
),
Self::IndexAllocationFailed { elements } => write!(
formatter,
"could not reserve {elements} converted embedding indices"
),
}
}
}
impl Error for EmbeddingError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Initialization(error) => Some(error),
Self::Autodiff(error) => Some(error),
Self::TokenShape(error) => Some(error),
_ => None,
}
}
}
impl From<InitializationError> for EmbeddingError {
fn from(error: InitializationError) -> Self {
Self::Initialization(error)
}
}
impl From<TensorAutodiffError> for EmbeddingError {
fn from(error: TensorAutodiffError) -> Self {
Self::Autodiff(error)
}
} Public callers obtain an Embedding through new, from_parameter, or by
cloning an already validated layer. The constructors establish a nonempty
rank-two table, cloning preserves it, and private fields prevent callers from
replacing it. The construction code records the table’s vocabulary size
and embedding width once:
rust/crates/llm-from-scratch/src/nn/embedding.rs#embedding-layer /// One named trainable `[vocabulary_size, embedding_width]` token table.
#[derive(Debug)]
pub struct Embedding {
table: NamedParameter,
vocabulary_size: usize,
embedding_width: usize,
}
impl Clone for Embedding {
/// Clones the layer handle while preserving the table's tape-leaf identity.
fn clone(&self) -> Self {
Self {
table: self.table.clone(),
vocabulary_size: self.vocabulary_size,
embedding_width: self.embedding_width,
}
}
}
impl Embedding {
/// Initializes one table transactionally with Chapter 17's shape-based policy.
///
/// The complete parameter name is used as supplied. Validation checks the
/// name before the vocabulary and width, and every error preserves `rng`.
pub fn new(
parameter_name: impl Into<String>,
vocabulary_size: usize,
embedding_width: usize,
rng: &mut SplitMix64,
) -> Result<Self, EmbeddingError> {
let mut trial = rng.clone();
let table = NamedParameter::xavier_uniform(
parameter_name,
vocabulary_size,
embedding_width,
&mut trial,
)
.map_err(|error| match error {
InitializationError::ZeroFanIn => EmbeddingError::EmptyVocabulary,
InitializationError::ZeroFanOut => EmbeddingError::ZeroEmbeddingWidth,
other => EmbeddingError::Initialization(other),
})?;
let embedding = Self::from_parameter(table)?;
*rng = trial;
Ok(embedding)
}
/// Gives embedding semantics to an existing named trainable rank-two table.
pub fn from_parameter(table: NamedParameter) -> Result<Self, EmbeddingError> {
let shape = table.tensor().shape();
if shape.len() != 2 {
return Err(EmbeddingError::TableRank { rank: shape.len() });
}
if shape[0] == 0 {
return Err(EmbeddingError::EmptyVocabulary);
}
if shape[1] == 0 {
return Err(EmbeddingError::ZeroEmbeddingWidth);
}
Ok(Self {
table,
vocabulary_size: shape[0],
embedding_width: shape[1],
})
}
} Embedding::forward is the public boundary for token IDs supplied as u32.
Because construction has already established the table shape , the
method performs these checks in a fixed order:
- compute the number of positions described by
token_shape, rejecting an invalid or overflowing shape; - require
token_ids.len()to equal that position count; and - scan the IDs in flat order, rejecting the first
u32value that cannot name one of the table’s rows.
Only after all IDs pass does the method reserve an owned Vec<usize> and convert
the selectors. After the tape establishes that the table operand is available,
the method creates Chapter 16’s crate-private RowGatherPlan. The plan owns the
converted selectors and their logical shape and derives the output shape. Its
trusted constructor does not rescan table rank, selector count, or selector
bounds. The constructor-or-clone invariant supplies the table rank, while this
forward call has just established the selector shape, count, and bounds:
rust/crates/llm-from-scratch/src/nn/embedding.rs#embedding-forward-boundary impl Embedding {
/// Selects one table row per `u32` token ID and appends the feature axis.
///
/// Token IDs remain integer selectors rather than differentiable operands.
/// After this boundary validates and converts them, the shared gather kernel
/// consumes the sealed facts without scanning the selectors again.
pub fn forward(
&self,
token_ids: &[u32],
token_shape: &[usize],
) -> Result<TensorValue, EmbeddingError> {
let (_, expected) =
checked_row_major_layout(token_shape).map_err(EmbeddingError::TokenShape)?;
if token_ids.len() != expected {
return Err(EmbeddingError::TokenCountMismatch {
expected,
actual: token_ids.len(),
});
}
for (position, &id) in token_ids.iter().enumerate() {
let valid = usize::try_from(id)
.ok()
.is_some_and(|index| index < self.vocabulary_size);
if !valid {
return Err(EmbeddingError::TokenIdOutOfBounds {
position,
id,
vocabulary_size: self.vocabulary_size,
});
}
}
let mut indices = Vec::new();
indices
.try_reserve_exact(expected)
.map_err(|_| EmbeddingError::IndexAllocationFailed { elements: expected })?;
for &id in token_ids {
indices.push(usize::try_from(id).expect("validated u32 token ID must fit usize"));
}
self.table
.tensor()
.gather_rows_with_plan(move |table| {
RowGatherPlan::from_validated_indices(table, indices, token_shape.to_vec())
})
.map_err(EmbeddingError::Autodiff)
}
} This trust is narrow. The public TensorValue::gather_rows method still checks
table rank, selector shape, selector count, and the first out-of-range selector
for callers that supply raw inputs. A valid plan also does not promise that
memory is already available: allocating the output buffer can still fail. Once
allocation succeeds, the shared kernel performs the row copies and saves the
same plan facts for reverse scatter-add. The embedding layer therefore avoids a
second selector-validation scan without duplicating either the lookup or the VJP.
Only ownership and reuse of checked facts change; output values and shapes,
failure precedence, the lookup equation, saved VJP facts, and repeated-row
gradient addition stay the same.
The exact nonuniform reverse seed makes repeated accumulation observable:
rust/demos/ch18-token-embeddings/src/lib.rs#repeated-token-gradient let upstream = Tensor::from_vec(vec![1, 3, 2], UPSTREAM_VALUES.to_vec())?;
output.backward_with_seed(&upstream.view(), GraphRetention::Retain)?;
let table_gradient = embedding
.table()
.tensor()
.gradient_snapshot()
.expect("trainable table stores its gradient"); Construction composes with Chapter 17’s shape-based matrix initializer by passing and as the table’s two dimensions. That is an explicit initialization convention, not something derived from embedding lookup or required by the Transformer paper. Another implementation can choose a different initializer without changing the forward or reverse equations. The same seed and request reproduce values, while separate constructions remain distinct leaves. Cloning one layer deliberately preserves its parameter identity:
rust/demos/ch18-token-embeddings/src/lib.rs#initialized-token-embedding let mut first_rng = SplitMix64::from_seed(18);
let mut second_rng = SplitMix64::from_seed(18);
let initialized = Embedding::new("token_embedding.weight", 4, 2, &mut first_rng)?;
let reproduced = Embedding::new("token_embedding.weight", 4, 2, &mut second_rng)?;
let initialized_reproducible =
*initialized.table().tensor().value() == *reproduced.table().tensor().value();
let clone_same_node = initialized
.table()
.tensor()
.is_same_node(initialized.clone().table().tensor()); The executable example also checks an empty ID shape and the first out-of-bounds ID:
rust/demos/ch18-token-embeddings/src/lib.rs#embedding-edge-cases let empty_output = embedding.forward(&[], &[0])?.value_snapshot();
let bounds_rejected = matches!(
embedding.forward(&[4], &[1]),
Err(EmbeddingError::TokenIdOutOfBounds {
position: 0,
id: 4,
vocabulary_size: 4,
})
); Its printed evidence contains the selected rows and accumulated table gradient. A scalar ID shape produces shape ; empty leading shapes append the width without producing values. Shape and count errors are reported before the first invalid selector. An all-coordinate finite-difference probe with step agrees with the analytic table gradient within absolute tolerance .
rust/demos/ch18-token-embeddings/src/main.rs#learner-token-embeddings-output let report = learner_report()?;
println!(
"table: {} shape={}",
report.table_name,
shape(&report.table_shape)
);
println!(
"ids: shape={} values={}",
shape(&report.token_shape),
report
.token_ids
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(",")
);
println!(
"output: shape={} values={}",
shape(report.output.shape()),
fixed_list(report.output.as_slice())
);
println!(
"one-hot multiplication equals lookup: {}",
report.one_hot_matches
);
println!(
"upstream: shape={} values={}",
shape(report.upstream.shape()),
fixed_list(report.upstream.as_slice())
);
println!(
"table gradient: shape={} values={}",
shape(report.table_gradient.shape()),
fixed_list(report.table_gradient.as_slice())
); Follow every selection back to its shared row
The forward half aligns each position with its token ID, one-hot identity, selected table row, and output vector. The reverse half aligns each upstream vector with the row that receives it. Row appears at two positions, so the diagram makes the difference visible: its value is copied to two outputs in the forward pass, while both gradient contributions add into one parameter row in the reverse pass.
rust/demos/ch18-token-embeddings/src/diagram_trace.rs#token-embeddings-trace pub fn render_trace() -> Result<String, Box<dyn Error>> {
let embedding = known_embedding();
let output = embedding.forward(&TOKEN_IDS, &TOKEN_SHAPE)?;
let upstream = Tensor::from_vec(vec![1, 3, 2], UPSTREAM_VALUES.to_vec())?;
output.backward_with_seed(&upstream.view(), GraphRetention::Retain)?;
let gradient = embedding
.table()
.tensor()
.gradient()
.expect("trainable table stores its gradient");
let (one_hot_rows, baseline) = explicit_one_hot_product(&known_table(), &TOKEN_IDS);
assert_eq!(baseline, output.value().as_slice());
let mut repeated_ids = TOKEN_IDS
.iter()
.copied()
.filter(|candidate| TOKEN_IDS.iter().filter(|id| *id == candidate).count() > 1)
.collect::<Vec<_>>();
repeated_ids.sort_unstable();
repeated_ids.dedup();
assert_eq!(
repeated_ids.len(),
1,
"fixture must contain one repeated token ID"
);
let repeated_id = repeated_ids[0];
let repeated_positions = TOKEN_IDS
.iter()
.enumerate()
.filter_map(|(position, id)| (*id == repeated_id).then_some(position))
.collect::<Vec<_>>();
let mut trace = String::new();
writeln!(trace, "TRACE token-embeddings-v1 BEGIN")?;
writeln!(
trace,
"FIXTURE name=known-table-repeated-id parameter={} vocabulary={} width={} table-shape={} id-shape={} output-shape={} upstream-shape={} gradient-shape={} accumulation=scatter-add",
embedding.table().name(),
embedding.vocabulary_size(),
embedding.embedding_width(),
shape(embedding.table().tensor().shape().as_slice()),
shape(&TOKEN_SHAPE),
shape(output.shape().as_slice()),
shape(upstream.shape()),
shape(gradient.shape()),
)?;
writeln!(
trace,
"IDS values={} repeated-id={repeated_id} repeated-flat-positions={}",
token_list(&TOKEN_IDS),
integer_list(&repeated_positions),
)?;
for row in 0..TABLE_SHAPE[0] {
let uses = TOKEN_IDS
.iter()
.filter(|&&id| usize::try_from(id).ok() == Some(row))
.count();
let state = match uses {
0 => "unused",
1 => "selected-once",
_ => "selected-repeated",
};
let start = row * embedding.embedding_width();
writeln!(
trace,
"TABLE row={row} uses={uses} state={state} values={}",
fixed_list(
&embedding.table().tensor().value().as_slice()
[start..start + embedding.embedding_width()]
)
)?;
}
for (flat, (&id, one_hot)) in TOKEN_IDS.iter().zip(&one_hot_rows).enumerate() {
let start = flat * embedding.embedding_width();
let coordinate = row_major_coordinate(flat, &TOKEN_SHAPE);
let uses = TOKEN_IDS
.iter()
.filter(|&&candidate| candidate == id)
.count();
writeln!(
trace,
"LOOKUP flat={flat} coordinate={} id={id} sharing={} one-hot={} selected-row={id} output={} upstream={}",
integer_list(&coordinate),
if uses > 1 {
"repeated-row"
} else {
"single-row"
},
integer_list(
&one_hot
.iter()
.map(|&value| usize::from(value))
.collect::<Vec<_>>()
),
fixed_list(&output.value().as_slice()[start..start + embedding.embedding_width()]),
fixed_list(&UPSTREAM_VALUES[start..start + embedding.embedding_width()])
)?;
}
for row in 0..TABLE_SHAPE[0] {
let positions = TOKEN_IDS
.iter()
.enumerate()
.filter_map(|(position, &id)| {
(usize::try_from(id).ok() == Some(row)).then_some(position)
})
.collect::<Vec<_>>();
let positions_text = if positions.is_empty() {
"none".to_owned()
} else {
integer_list(&positions)
};
let contributions_text = if positions.is_empty() {
"none".to_owned()
} else {
positions
.iter()
.map(|position| {
let start = position * TABLE_SHAPE[1];
fixed_list(&UPSTREAM_VALUES[start..start + TABLE_SHAPE[1]])
})
.collect::<Vec<_>>()
.join("|")
};
let start = row * embedding.embedding_width();
let rule = match positions.len() {
0 => "unused-zero",
1 => "single-copy",
_ => "repeated-sum",
};
writeln!(
trace,
"ROW-GRADIENT row={row} flat-positions={positions_text} contributions={contributions_text} rule={rule} accumulated={}",
fixed_list(&gradient.as_slice()[start..start + embedding.embedding_width()])
)?;
}
writeln!(trace, "TRACE token-embeddings-v1 END")?;
Ok(trace)
} Read the forward and reverse halves together. The repeated state is marked by words, a diamond, and a double rule rather than color alone, so row sharing stays visible independently of the color scheme.
Predict before checking the executable evidence
- Predict all six output values for IDs .
- Write the length-four one-hot row for ID and multiply it by .
- Predict every table-gradient value for the declared upstream seed.
- Explain why row receives a sum rather than two independent parameter gradients.
- Predict the output shapes for ID shapes , , and when .
- Put an invalid token shape, an ID-count mismatch, and an out-of-range ID in reporting order; then find the first invalid ID in for a four-row table.
- Explain why
Embedding::forwardmay hand its checked IDs to a trusted plan while the public generic gather method must validate raw selectors. What allocation can still fail after the plan exists? - Predict whether cloning an embedding layer creates a new trainable leaf.
- Explain why IDs receive no gradient and why adjacent ID numbers imply no semantic similarity.
- Decide whether Bengio et al. require an explicitly materialized one-hot implementation.
- Decide whether a repeated occurrence owns a new embedding parameter.
Check the predictions
- The output is .
- ID is , so multiplying it by yields .
- The table gradient is .
- Both occurrences selected the same row- leaf, so and add into it.
- The output shapes are , , and .
- The method checks the token shape first, the exact ID count second, and the IDs in flat order third. ID at flat position is the first invalid selector because valid rows are through .
- Every
Embeddingvalue contains the rank-two table established by its public constructors, andforwardhas just checked shape, count, and bounds before converting the IDs. The trusted plan owns those facts. A generic public caller has established none of them, soTensorValue::gather_rowsmust perform its complete validation. Allocating the output tensor’s value buffer can still fail after the plan exists. - No. A clone is another handle to the same named table leaf.
- IDs are discrete selectors outside the tape, and their numeric assignment carries no geometry.
- No. One-hot multiplication is used here only as an algebraic explanation.
- No. Repetition reuses one table row; later position handling distinguishes occurrences without creating occurrence-specific embedding parameters.
Hand the final feature axis to a learned projection
The cumulative model can now turn token-ID tensors into differentiable feature tensors that append one embedding-width axis while keeping one shared named vocabulary-by-feature parameter. Chapter 19 treats that final embedding width as its input width and mixes features with a learned projection; lookup selects rows, while a linear layer combines coordinates.
This is the decoder’s numeric entrance, not yet a position-aware representation. Chapter 19 preserves all leading batch and sequence axes while applying a learned matrix to the final feature axis. Later chapters make attention position-aware by rotating projected queries and keys with RoPE; repeated occurrences still share one embedding-table row.