exTransformer.cpp

Train a tiny Transformer language model. Learn a 300-token BPE vocabulary, train a causal Transformer for 300 optimizer steps, verify that cross-entropy falls, and generate text from the trained model.

ExSDK source-ownedC++Ml

Source

Ml · Vulkan compute + autograd
sdk/cpp/examples/ml/transformer.cpp

Example

1#include <oa/oa.h>
2
3namespace {
4constexpr oa::I32 VocabSize = 300;
5constexpr oa::I32 ContextLength = 16;
6constexpr oa::I32 ModelWidth = 32;
7constexpr oa::I32 HiddenWidth = 64;
8constexpr oa::I32 BatchSize = 64;
9constexpr oa::I32 TrainingSteps = 300;
10constexpr const char* Corpus =
11 "to be or not to be that is the question whether tis nobler in the mind "
12 "to suffer the slings and arrows of outrageous fortune or to take arms "
13 "against a sea of troubles and by opposing end them to be or not to be "
14 "that is the question whether tis nobler in the mind to suffer the slings "
15 "and arrows of outrageous fortune or to take arms against a sea of "
16 "troubles and by opposing end them to be or not to be that is the question "
17 "whether tis nobler in the mind to suffer the slings and arrows of "
18 "outrageous fortune or to take arms against a sea of troubles and by "
19 "opposing end them ";
20
21oa::Matrix tokenMatrix(const oa::Vector<oa::I32>& tokens, oa::I32 batch) {
22 return oa::FnMatrix::fromInt32(
23 oa::Span<const oa::I32>(tokens.data(), tokens.size()),
24 {batch, ContextLength}, oa::ScalarType::UInt32
25 );
26}
27
28void nextBatch(const oa::Vector<oa::I32>& tokens, oa::I64& cursor,
29 oa::Matrix& input, oa::Matrix& target) {
30 const oa::I64 limit = static_cast<oa::I64>(tokens.size()) - ContextLength - 1;
31 oa::Vector<oa::I32> x(static_cast<oa::Usize>(BatchSize * ContextLength));
32 oa::Vector<oa::I32> y(x.size());
33 for (oa::I32 batch = 0; batch < BatchSize; ++batch) {
34 const oa::I64 start = (cursor + static_cast<oa::I64>(batch) * 7) % limit;
35 for (oa::I32 position = 0; position < ContextLength; ++position) {
36 const auto output = static_cast<oa::Usize>(batch * ContextLength + position);
37 x[output] = tokens[static_cast<oa::Usize>(start + position)];
38 y[output] = tokens[static_cast<oa::Usize>(start + position + 1)];
39 }
40 }
41 cursor = (cursor + BatchSize) % limit;
42 input = tokenMatrix(x, BatchSize);
43 target = tokenMatrix(y, BatchSize);
44}
45
46oa::String generate(oa::NnTransformer& model, const oa::BpeTokenizer& tokenizer) {
47 const oa::String prompt = "to be";
48 auto promptTokens = tokenizer.encode(prompt.cStr());
49 oa::Vector<oa::I32> context(static_cast<oa::Usize>(ContextLength), 0);
50 const auto copied = oa::min<oa::Usize>(promptTokens.size(), context.size());
51 for (oa::Usize index = 0; index < copied; ++index) context[index] = promptTokens[index];
52 oa::I32 filled = oa::max<oa::I32>(1, static_cast<oa::I32>(copied));
53 oa::String result = prompt;
54 for (oa::I32 index = 0; index < 32; ++index) {
55 auto logits = model.forward(tokenMatrix(context, 1));
56 auto row = oa::FnMatrix::slice(logits, 0, filled - 1, filled);
57 const auto next = static_cast<oa::I32>(oa::FnMatrix::argmax(row.reshape({VocabSize})));
58 result += tokenizer.decode({next});
59 if (filled < ContextLength) {
60 context[static_cast<oa::Usize>(filled++)] = next;
61 } else {
62 for (oa::Usize token = 1; token < context.size(); ++token) context[token - 1] = context[token];
63 context[ContextLength - 1] = next;
64 }
65 }
66 return result;
67}
68} // namespace
69
70OA_MAIN("ExampleMlTransformer") {
71 oa::FnMatrix::setRngSeed(20260714ULL);
72 oa::BpeTokenizer tokenizer(VocabSize);
73 tokenizer.train(Corpus, VocabSize - 256);
74 if (tokenizer.vocabSize() != VocabSize) return 1;
75 const auto corpusTokens = tokenizer.encode(Corpus);
76
77 oa::NnTransformer model(VocabSize, ContextLength, ModelWidth, HiddenWidth);
78 auto parameters = model.allParameterPtrs();
79 oa::AdamW optimizer(parameters, 0.01F);
80 oa::MetricLoss lossMetric;
81 oa::CbProgressBar progress;
82 oa::CbSummary summary;
83 progress.addMetric(&lossMetric);
84 oa::ItTrainingConfig config;
85 config.totalSteps = TrainingSteps;
86 config.batchSize = BatchSize;
87 config.sequenceLength = ContextLength;
88 config.sequenceUnit = "token";
89 config.timerName = "example_transformer_step";
90 oa::ItTraining training(engine, optimizer, config);
91 training.addMetric(&lossMetric);
92 training.addCallback(&progress);
93 training.addCallback(&summary);
94
95 oa::print("\nOA SDK Example — BPE Transformer · all-position LM");
96 oa::print("Tokenizer: byte BPE · vocab={} · context={}", VocabSize, ContextLength);
97 oa::print("Model: NnTransformer(width={}, hidden={}, layers=1, heads=1)",
98 ModelWidth,
99 HiddenWidth
100 );
101 oa::print("Params: {} · AdamW(lr=0.01)", static_cast<long long>(model.numParameters()));
102 oa::print("Training: {} steps · batch={} · sequence={} tokens",
103 TrainingSteps,
104 BatchSize,
105 ContextLength
106 );
107
108 oa::I64 cursor = 0;
109 oa::Matrix input;
110 oa::Matrix target;
111 oa::F32 initialLoss = 0.0F;
112 while (not training.isDone()) {
113 nextBatch(corpusTokens, cursor, input, target);
114 optimizer.zeroGrad();
115 oa::GradientTape tape;
116 auto logits = model.forward(input);
117 auto loss = oa::FnLoss::crossEntropy(logits, target.reshape({target.numElements()}));
118 tape.backward(loss);
119 training.next(loss);
120 if (training.index() == 1) {
121 initialLoss = training.lastLoss();
122 }
123 }
124 if (not training.finish().isOk()) {
125 return 1;
126 }
127
128 const oa::F32 finalLoss = training.lastLoss();
129 if (not (finalLoss < initialLoss)) {
130 return 1;
131 }
132 const auto generated = generate(model, tokenizer);
133 oa::print("Transformer training verified: vocab=300, steps=300");
134 oa::print("Loss: {:.4f} -> {:.4f}", initialLoss, finalLoss);
135 oa::print("Generated: {}", generated.cStr());
136 return 0;
137}
138

C++ / Python

C++. Selected live progress redraws and the complete tutorial-style summary from one checked Intel Iris Xe run. Timings are single-run evidence, not a benchmark claim.

Extransformer.cpp · output

$ OA_LOG_TRAINING_PHASES=1 bin/release/sdk/examples/ml/exampleMlTransformer
14:15:29.239 [INFO ] [RT ] ComputeDevice (0): Intel(R) Iris(R) Xe Graphics (TGL GT2), vulkan 1.4.354, 11 GB
OA SDK Example — BPE Transformer · all-position LM
Tokenizer: byte BPE · vocab=300 · context=16
Model: NnTransformer(width=32, hidden=64, layers=1, heads=1)
Params: 28620 · AdamW(lr=0.01)
Training: 300 steps · batch=64 · sequence=16 tokens
2/300 |▏░░░░░░░░░| 0.05s · 23.57 ms/step · 2.72K sample/s · 43.45K token/s · cross_entropy: 5.5437
50/300 |█▋░░░░░░░░| 0.37s · 7.43 ms/step · 8.61K sample/s · 137.84K token/s · cross_entropy: 1.1568
153/300 |█████▏░░░░| 1.06s · 6.95 ms/step · 9.21K sample/s · 147.33K token/s · cross_entropy: 0.4138
300/300 |██████████| 2.02s · 6.74 ms/step · 9.50K sample/s · 151.94K token/s · cross_entropy: 0.2331
Summary:
loss: initial 5.842306 · final 0.041481 · mean 0.233055
Wall: 6.74 ms/step · 9.50K sample/s · 151.94K token/s
GPU: mean 5.139 ms/step · p50 4.444 · p95 7.878 · 12.45K sample/s · 199.26K token/s · wall-GPU gap 24%
run: 2.02s · 300 steps · batch 64 · sequence 16 token/sample
14:15:31.299 [INFO ] [ML ] training phases: steps=300 total=6.735 ms/step body=0.640 optimizer=0.037 compile=0.048 record=0.124 submit=0.046 wait=5.617 scalar_metric=0.015 callbacks=0.007 unaccounted=0.202
Transformer training verified: vocab=300, steps=300
Loss: 5.8423 -> 0.0415
Generated: to bepposing end them to be or not to be that is the question whether tis

Python. Selected live progress redraws and the complete Python summary. It resolves to the same Vulkan graph, seed, losses, and generated continuation as C++.

Extransformer.py · output

$ OA_LOG_TRAINING_PHASES=1 .venv/bin/python sdk/py/examples/ml/transformer.py
14:15:36.804 [INFO ] [RT ] ComputeDevice (0): Intel(R) Iris(R) Xe Graphics (TGL GT2), vulkan 1.4.354, 11 GB
OA SDK Example — BPE Transformer · all-position LM
Tokenizer: byte BPE · vocab=300 · context=16
Model: NnTransformer(width=32, hidden=64, layers=1, heads=1)
Params: 28620 · AdamW(lr=0.01)
Training: 300 steps · batch=64 · sequence=16 tokens
3/300 |▏░░░░░░░░░| 0.05s · 15.11 ms/step · 4.24K sample/s · 67.79K token/s · cross_entropy: 5.2941
53/300 |█▊░░░░░░░░| 0.39s · 7.31 ms/step · 8.76K sample/s · 140.09K token/s · cross_entropy: 1.0947
151/300 |█████░░░░░| 1.04s · 6.89 ms/step · 9.29K sample/s · 148.66K token/s · cross_entropy: 0.4187
300/300 |██████████| 2.04s · 6.79 ms/step · 9.42K sample/s · 150.75K token/s · cross_entropy: 0.2331
Summary:
loss: initial 5.842306 · final 0.041481 · mean 0.233055
Wall: 6.79 ms/step · 9.42K sample/s · 150.74K token/s
GPU: mean 4.895 ms/step · p50 4.421 · p95 7.812 · 13.07K sample/s · 209.19K token/s · wall-GPU gap 28%
run: 2.04s · 300 steps · batch 64 · sequence 16 token/sample
14:15:38.875 [INFO ] [ML ] training phases: steps=300 total=6.784 ms/step body=0.937 optimizer=0.043 compile=0.049 record=0.126 submit=0.047 wait=5.344 scalar_metric=0.023 callbacks=0.014 unaccounted=0.201
Transformer training verified: vocab=300, steps=300
Loss: 5.8423 -> 0.0415
Generated: to bepposing end them to be or not to be that is the question whether tis