Files
2026-07-13 12:37:59 +08:00

304 lines
12 KiB
C

/*
Tests our DataLoader
compile and run as (from dev/test directory)
gcc -O3 -I../../llmc -o test_dataloader test_dataloader.c -lm && ./test_dataloader
TODOs:
- test load/save state of DataLoader
*/
#include <unistd.h>
#include "../../llmc/dataloader.h"
#define SHARD_NAME_LEN 64
char shard_name[SHARD_NAME_LEN];
const int num_tokens = 140;
int num_shards = 4;
void check_range(const int *tokens, const int start, const int end, const char *file, int line) {
// checks that the tokens[0, ... end-start] are the range [start, end)
int n = end - start;
for (int i = 0; i < n; i++) {
int token = tokens[i];
if (token != start + i) {
fprintf(stderr, "Error: tokens[%d] = %d, expected %d\n", i, token, start + i);
fprintf(stderr, "Error details:\n");
fprintf(stderr, " File: %s\n", file);
fprintf(stderr, " Line: %d\n", line);
exit(EXIT_FAILURE);
}
}
// printf("tokens in range [%d, %d) OK\n", start, end);
}
#define checkRange(tokens, start, end) check_range(tokens, start, end, __FILE__, __LINE__)
void check_equals(const int *tokens, const int n, const int expected, const char *file, int line) {
// checks that the tokens[0, ... n] are all equal to expected
for (int i = 0; i < n; i++) {
int token = tokens[i];
if (token != expected) {
fprintf(stderr, "Error: tokens[%d] = %d, expected %d\n", i, token, expected);
fprintf(stderr, "Error details:\n");
fprintf(stderr, " File: %s\n", file);
fprintf(stderr, " Line: %d\n", line);
exit(EXIT_FAILURE);
}
}
// printf("tokens all equal to %d OK\n", expected);
}
#define checkEquals(tokens, n, expected) check_equals(tokens, n, expected, __FILE__, __LINE__)
void test_simple(void) {
/*
Tests the simplest DataLoader functionality:
- multi-shard
- single-process
- not shuffled
DataLoader should just return all the tokens in order
*/
printf("test_simple... ");
int B = 4;
int T = 8;
int process_rank = 0;
int num_processes = 1;
int should_shuffle = 0;
snprintf(shard_name, SHARD_NAME_LEN, "shard_????.bin");
DataLoader loader;
dataloader_init(&loader, shard_name, B, T, process_rank, num_processes, should_shuffle);
int batches_fit = num_tokens / (B * T); // number of batches that fit per shard
int BT = B * T;
int num_epochs = 4;
for (int e = 0; e < num_epochs; e++) { // epoch
for (int s = 0; s < num_shards; s++) { // shard
int start = s * num_tokens;
for (int b = 0; b < batches_fit; b++) { // batch
dataloader_next_batch(&loader);
checkRange(loader.inputs, start, start + BT);
checkRange(loader.targets, start + 1, start + BT + 1);
start += BT;
}
}
}
dataloader_free(&loader);
printf("OK\n");
}
void test_multiprocess_simple(void) {
/*
Same as simple above, but using 2 processes.
(which we of course use in a serial, single process way here)
The DataLoaders simply pull chunks of consecutive tokens, so
we expect them to alternate in the "token space".
*/
printf("test_multiprocess_simple... ");
int B = 4;
int T = 8;
int num_processes = 2;
int should_shuffle = 0;
snprintf(shard_name, SHARD_NAME_LEN, "shard_????.bin");
DataLoader loader0, loader1;
dataloader_init(&loader0, shard_name, B, T, 0, num_processes, should_shuffle);
dataloader_init(&loader1, shard_name, B, T, 1, num_processes, should_shuffle);
int batches_fit = num_tokens / (B * T * num_processes); // number of batches that fit per shard
int BT = B * T;
int num_epochs = 4;
for (int e = 0; e < num_epochs; e++) { // epoch
for (int s = 0; s < num_shards; s++) { // shard
int start = s * num_tokens;
for (int b = 0; b < batches_fit; b++) { // batch
dataloader_next_batch(&loader0);
dataloader_next_batch(&loader1);
checkRange(loader0.inputs, start, start + BT);
checkRange(loader1.inputs, start + BT, start + 2*BT);
checkRange(loader0.targets, start + 1, start + BT + 1);
checkRange(loader1.targets, start + BT + 1, start + 2*BT + 1);
start += 2*BT;
}
}
}
dataloader_free(&loader0);
dataloader_free(&loader1);
printf("OK\n");
}
void test_shuffled(void) {
/*
Tests the DataLoader when using shuffled:
- multi-shard
- single-process
- shuffled!
DataLoader should return all the tokens, but in randperm order.
So all we check is that we see all the tokens we expect to see,
the correct number of times.
*/
printf("test_shuffled... ");
int B = 4;
int T = 8;
int process_rank = 0;
int num_processes = 1;
int should_shuffle = 1; // should shuffle bit turn on
snprintf(shard_name, 64, "shard_????.bin");
DataLoader loader;
dataloader_init(&loader, shard_name, B, T, process_rank, num_processes, should_shuffle);
// get batches from the dataloader and keep stats on what tokens we see
int total_tokens = num_shards * num_tokens;
int *num_seen_inputs = (int *)calloc(total_tokens, sizeof(int));
int *num_seen_targets = (int *)calloc(total_tokens, sizeof(int));
int batches_fit = num_tokens / (B * T); // number of batches that fit per shard
int BT = B * T;
int num_epochs = 4;
for (int e = 0; e < num_epochs; e ++) { // epoch
for (int s = 0; s < num_shards; s++) { // shard
int start = s * num_tokens;
for (int b = 0; b < batches_fit; b++) { // batch
dataloader_next_batch(&loader);
// count up the tokens we see
for (int i = 0; i < BT; i++) {
int input_token = loader.inputs[i];
int target_token = loader.targets[i];
assert(input_token >= 0 && input_token < total_tokens);
assert(target_token >= 0 && target_token < total_tokens);
num_seen_inputs[input_token]++;
num_seen_targets[target_token]++;
}
start += BT;
}
}
}
// verify that we saw all the tokens the correct number of times
int tokens_fit = batches_fit * BT; // number of tokens that fit per shard
for (int s = 0; s < num_shards; s++) {
int start = s * num_tokens;
// verify the inputs counts for this shard:
// - the first tokens_fit should have been seen num_epochs times
// - the rest of the tokens in that should should have been seen zero times
checkEquals(num_seen_inputs + start, tokens_fit, num_epochs);
checkEquals(num_seen_inputs + start + tokens_fit, num_tokens - tokens_fit, 0);
// verify the target counts. same thing but offset by 1
checkEquals(num_seen_targets + start + 1, tokens_fit, num_epochs);
checkEquals(num_seen_targets + start + 1 + tokens_fit,
(s == (num_shards - 1)) ? num_tokens - tokens_fit - 1 : num_tokens - tokens_fit,0);
}
dataloader_free(&loader);
free(num_seen_inputs);
free(num_seen_targets);
printf("OK\n");
}
void test_multiprocess_shuffled(void) {
/*
Tests the DataLoader when using both multiprocess and shuffled:
- multi-shard
- multi-process
- shuffled!
DataLoaders should return all the tokens, but in randperm order.
So all we check is that we see all the tokens we expect to see,
the correct number of times, over multiple epochs.
*/
printf("test_multiprocess_shuffled... ");
int B = 4;
int T = 8;
const int num_processes = 2;
int should_shuffle = 0;
snprintf(shard_name, SHARD_NAME_LEN, "shard_????.bin");
DataLoader loaders[num_processes];
for (int i = 0; i < num_processes; i++) {
dataloader_init(&loaders[i], shard_name, B, T, i, num_processes, should_shuffle);
}
// get batches from the dataloader and keep stats on what tokens we see
int total_tokens = num_shards * num_tokens;
int *num_seen_inputs = (int *)calloc(total_tokens, sizeof(int));
int *num_seen_targets = (int *)calloc(total_tokens, sizeof(int));
int batches_fit = num_tokens / (B * T * num_processes); // number of batches that fit per shard
int BT = B * T;
int num_epochs = 4;
for (int e = 0; e < num_epochs; e ++) { // epoch
for (int s = 0; s < num_shards; s++) { // shard
int start = s * num_tokens;
for (int b = 0; b < batches_fit; b++) { // batch
for (int n = 0; n < num_processes; n++) { // dataloader
DataLoader *loader = &loaders[n];
dataloader_next_batch(loader);
// count up the tokens we see
for (int i = 0; i < BT; i++) {
int input_token = loader->inputs[i];
int target_token = loader->targets[i];
assert(input_token >= 0 && input_token < total_tokens);
assert(target_token >= 0 && target_token < total_tokens);
num_seen_inputs[input_token]++;
num_seen_targets[target_token]++;
}
start += BT;
}
}
}
}
// verify that we saw all the tokens the correct number of times
int tokens_fit = batches_fit * (B * T * num_processes); // number of tokens that fit per shard
for (int s = 0; s < num_shards; s++) {
int start = s * num_tokens; // token id that starts this shard
// verify the inputs counts for this shard:
// - the first tokens_fit should have been seen num_epochs times
// - the rest of the tokens in that should should have been seen zero times
checkEquals(num_seen_inputs + start, tokens_fit, num_epochs);
checkEquals(num_seen_inputs + start + tokens_fit, num_tokens - tokens_fit, 0);
// verify the target counts. same thing but offset by 1
checkEquals(num_seen_targets + start + 1, tokens_fit, num_epochs);
checkEquals(num_seen_targets + start + 1 + tokens_fit,
(s == (num_shards - 1)) ? num_tokens - tokens_fit - 1 : num_tokens - tokens_fit,0);
}
// cleanup
for (int i = 0; i < num_processes; i++) {
dataloader_free(&loaders[i]);
}
free(num_seen_inputs);
free(num_seen_targets);
printf("OK\n");
}
int main(void) {
// generate a few dummy shards of data with incrementing tokens
int header[HEADER_SIZE];
uint16_t tokens[num_tokens];
for (int shard_id = 0; shard_id < num_shards; shard_id++) {
// ensure unique tokens across the shards for ez accounting below
int token_offset = shard_id * num_tokens;
for (int i = 0; i < num_tokens; i++) {
tokens[i] = token_offset + i;
}
// write the shard
snprintf(shard_name, SHARD_NAME_LEN, "shard_%04d.bin", shard_id);
header[0] = 20240520; // magic
header[1] = 1; // version
header[2] = num_tokens; // number of tokens within
FILE* shard_file = fopenCheck(shard_name, "wb");
fwrite(header, sizeof(int), HEADER_SIZE, shard_file);
fwrite(tokens, sizeof(uint16_t), num_tokens, shard_file);
fcloseCheck(shard_file);
printf("Wrote shard %s\n", shard_name);
}
test_simple();
test_multiprocess_simple();
test_shuffled();
test_multiprocess_shuffled();
// clean up the shards
for (int shard_id = 0; shard_id < num_shards; shard_id++) {
snprintf(shard_name, SHARD_NAME_LEN, "shard_%04d.bin", shard_id);
remove(shard_name);
}
return EXIT_SUCCESS;
}