zxell.ai
This is an internal project report edited for publication, translated from the Japanese original. Server operations and security details, and details of the corpus, are omitted; omissions are marked in the text.

Framework methods used in phase 1 — those directly involved in LLM training (by module)

Date: 2026-10-11 Commit: 866b7a2 (the code as of the completed phase-1 S baseline)

0. About this document

LibraryVersionModules that use itRole
SentencePiece (sentencepiece)0.2.2zxell_prep/, zxell_model/sample.pyTraining the BPE tokenizer, encoding, decoding
PyTorch (torch)2.14.0 (CPU build on the server, CUDA build cu128 on the RTX 3060 machine)zxell_model/Tensor operations, automatic differentiation, layer building blocks, optimization, mixed precision

1. zxell_prep/ (tokenizer and preprocessing)

In preprocessing, the only library directly involved in LLM training is SentencePiece. Three files use it.

1-1. train_compare_tokenizers.py — training and comparing tokenizers

CallUsed for
sentencepiece.SentencePieceTrainer.trainTraining the BPE tokenizer (two of them: 48k and 64k vocabularies)
sentencepiece.SentencePieceProcessorLoading the trained model
.encodeTurning the evaluation set into token sequences to measure characters/token per language

Main arguments passed to SentencePieceTrainer.train:

ArgumentValueMeaning
model_type"bpe"Train with BPE
vocab_size48,000 / 64,000Vocabulary size (48k was adopted)
character_coverage0.9995Share of characters covered by the vocabulary
byte_fallbackTrueUnknown characters fall back to bytes (no UNK is emitted)
split_digitsTrueDigits are split one at a time
normalization_rule_name"nmt_nfkc"Character normalization
max_sentence_length16,384Upper limit on the length treated as one sentence
input_sentence_size / shuffle_input_sentence2,000,000 / TrueUse 2 million sentences drawn at random from the training file

How the training file is built (apportioned by a per-language character cap, with ja thickened to about 19%) is in-house logic. The BPE training algorithm itself is left to SentencePiece (a decision from review10).

1-2. tokenize_full.py — full tokenization and sharding

CallUsed for
sentencepiece.SentencePieceProcessorLoading the adopted 48k model
.encodeTurning every article into a sequence of token IDs (the step that produces the training data itself)
.eos_idThe ID of the separator token (EOS) placed between articles
.vocab_sizeChecking that the vocabulary fits in uint16 (below 65,536)

Normalization, deduplication, the per-language split, article-level shuffling, cutting into shards of 50 million tokens, and verification right after writing are all in-house logic.

1-3. peek_shard.py — eyeballing shards

CallUsed for
sentencepiece.SentencePieceProcessorLoading the 48k model
.decodeTurning a shard's token sequence back into text for display (to check what is in the training data)
.eos_idFinding the boundaries between articles

1-4. Files that use no framework methods

parse_dump.py (building the snapshot), extract_sample.py (extracting samples), compute_boundaries.py (computing the split boundaries), and textprep.py (text cleanup) contain no calls to a framework directly involved in LLM training. All of their processing is in-house logic.


2. zxell_model/ (model and training)

2-1. The line between "in-house" and "framework"

The conclusion first: what was written in-house and what was left to PyTorch.

PartWritten in-houseLeft to PyTorch
RMSNormThe formula itself (multiply by the inverse square root of the mean square; computed after promoting to fp32)torch.rsqrt, .pow and .mean, the container for learned parameters nn.Parameter
RoPE (rotary position embedding)Building the frequency table, the formula that rotates the first and second halvestorch.arange, torch.outer, .cos, .sin, torch.cat
Self-attentionThe Q/K/V projection and the split into heads, applying RoPE, the output projectionThe attention computation itself: F.scaled_dot_product_attention(is_causal=True) (softmax(QKᵀ/√d)V and the causal mask. An implementation optimized for GPUs)
SwiGLU FFNThe structure (three linear layers — gate, up, down — and the multiplication)nn.Linear, the activation function F.silu
Transformer block and the GPT itselfThe Pre-Norm residual structure, stacking the layers, weight sharing between the embedding and the LM head, the initialization policynn.Embedding, nn.Linear, nn.ModuleList, the initializer nn.init.normal_
LossComputing it 4 sequences at a time, because the logits over a 48k vocabulary are largeCross-entropy F.cross_entropy, recomputation during backpropagation torch.utils.checkpoint.checkpoint
TrainingThe learning-rate schedule (warmup + cosine), deciding which parameters get weight decay, sampling the training dataAutomatic differentiation (loss.backward()), optimization (torch.optim.AdamW), gradient clipping, bf16 mixed precision (torch.autocast)
EvaluationAggregating per-language perplexity (assigning a language to each token)Per-token loss F.cross_entropy, torch.exp
GenerationThe autoregressive loop, temperature, top-p (cutting off by cumulative probability)F.softmax, .sort, .cumsum, .scatter_, torch.multinomial

What this shows

The second point, the attention computation, has never been explicitly confirmed as a decision. If it matters for the scope of "in-house implementation", it can be swapped for an in-house version and compared on speed and loss.

2-2. model.py — the GPT itself

CallUsed for
torch.nn.Module (inherited)Base class of every layer (RMSNorm, Attention, SwiGLU, Block, GPT)
torch.nn.ParameterRMSNorm's learned scale
torch.nn.EmbeddingToken embedding (48k vocabulary × d_model)
torch.nn.Linear ×6The Q/K/V projection, the attention output projection, the three SwiGLU layers, the LM head (all without bias)
torch.nn.ModuleListLining up as many Transformer blocks as there are layers
torch.nn.init.normal_ ×3Weight initialization (standard deviation 0.02; smaller, according to the number of layers, for projections added into the residual stream)
torch.nn.functional.scaled_dot_product_attentionAttention with a causal mask
torch.nn.functional.siluSwiGLU's activation function
torch.nn.functional.cross_entropyThe next-token prediction loss
torch.nn.functional.softmaxTurning logits into probabilities during generation
torch.utils.checkpoint.checkpointSplits LM head + loss into chunks and recomputes them during backpropagation (so the logits are not all kept; this fits training into 12GB of VRAM)
torch.ones / torch.zeros_likeRMSNorm's initial value / the container for reordering probabilities in top-p
torch.arange ×2, torch.outerRoPE's frequency table (position × frequency)
torch.rsqrtThe inverse square root in RMSNorm
torch.cat ×2Joining the rotated first and second halves in RoPE / appending generated tokens
torch.multinomialSampling during generation
torch.no_grad (decorator)No gradients are computed during generation

Main tensor methods:

MethodUsed for
.view, .transpose, .contiguous, .reshapeSplitting into heads and joining them back, reshaping for the loss
.split, .chunkSeparating Q/K/V, RoPE's first and second halves, chunking the loss
.float, .type_asPromote to fp32 for the computation, then return to the original type (bf16)
.pow, .mean, .cos, .sin, .unsqueezeThe RMSNorm and RoPE computations
.sort, .cumsum, .scatter_, .argmax, .sum, .allThe top-p cut-off, greedy selection, the stop condition
.new_zeros, .numelThe container for the loss total, parameter and token counts

nn.Module methods: .apply (applies the initialization to every layer), .named_parameters, .parameters.

2-3. data.py — building training batches and evaluation

CallUsed for
torch.from_numpy ×3Turning a token sequence cut from a shard into a tensor
torch.stack ×2Collecting the inputs (x) and the targets (y = the token one step ahead) into a batch
torch.zeros ×2Containers for the per-language loss totals and counts (fp64)
torch.autocastbf16 mixed precision during evaluation
torch.nn.functional.cross_entropyPer-token loss (reduction="none", so it can be aggregated per language)
torch.exp ×2Converting mean loss into perplexity
torch.no_grad (decorator)No gradients are computed during evaluation

Main methods: the tensor's .to(device) (transfer to the GPU), .reshape, .sum, .numel; the model's .eval and .train (switching between evaluation and training).

How the position to cut from is chosen (a shard is picked in proportion to its token count, then ctx+1 tokens from a random position) and the step that counts EOS tokens to find each token's language are in-house logic.

2-4. train.py — the training loop

CallUsed for
torch.optim.AdamWOptimization (β 0.9 / 0.95, weight decay 0.1; the fused implementation on CUDA)
torch.autocastbf16 mixed precision (on CUDA)
torch.nn.utils.clip_grad_norm_Gradient clipping (limit 1.0). The returned gradient norm is logged
torch.save, torch.loadSaving checkpoints and resuming (model weights, optimizer state, step count)
torch.manual_seedFixing the random seed (reproducible initialization)
torch.device, torch.cuda.is_availableUse CUDA when it is available

Main methods:

MethodUsed for
loss.backward()Automatic differentiation (computes the gradient of every parameter)
opt.step(), opt.zero_grad()Updating the parameters, resetting the gradients
opt.state_dict(), opt.load_state_dict()Saving and restoring the optimizer state
model.to(device), model.train(), model.state_dict(), model.load_state_dict()Placing the model on the GPU, training mode, saving and restoring the weights
.item()Taking the loss out as a number (for the log)
p.dim()Deciding which parameters get weight decay (only weights with 2 or more dimensions)

The learning-rate schedule (linear warmup → cosine decay, ending at 10%) is written as an in-house formula, without PyTorch's schedulers.

2-5. eval.py / sample.py

FileCallUsed for
eval.pytorch.load, torch.device, torch.cuda.is_availableLoads a checkpoint and reports per-language perplexity on val / test (the computation is the evaluation in data.py)
sample.pysentencepiece.SentencePieceProcessorLoading the 48k model
sample.py.encode, .bos_id / .decodeTurning the prompt into a token sequence / turning the generated tokens back into text
sample.pytorch.tensor, torch.manual_seed, torch.device, torch.cuda.is_availableTurning the prompt into a tensor, fixing the sampling seed

config.py (the definitions of the S / M / L configurations) and fetch_shards.py (fetching shards from the API) contain no calls to a framework directly involved in LLM training.


3. Summary

ModuleLibraryCore left to the libraryCore written in-house
zxell_prep/SentencePieceBPE training, encoding, decodingApportioning the training file, normalization, deduplication, the per-language split, shuffling, sharding, verification
zxell_model/PyTorch (+ SentencePiece only for generation)Tensor operations, automatic differentiation, AdamW, the core attention computation, cross-entropy, mixed precisionThe model's structure (RMSNorm, RoPE, SwiGLU, blocks, weight sharing), the learning-rate schedule, how batches are built, per-language evaluation, generation (top-p)

From the standpoint of "from scratch", the dependence on a framework is greatest in two places:

  1. Training the tokenizer (SentencePieceTrainer.train). Decided in review10.
  2. The core attention computation (F.scaled_dot_product_attention). The Q, K, V projections and RoPE are in-house, but softmax(QKᵀ/√d)V and the causal mask are PyTorch's implementation. The same computation could be replaced with a few lines of in-house code (matrix product + mask + softmax), at a cost in speed and memory.