Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,26 @@ The generated files can be passed directly to the corresponding executables:

```bash
./build/mnist \
--model cnn \
--device cpu \
--dataset data/mnist
```

`--model` selects the network, `cnn` (default) or `mlp`. Launching through
`infini_run` distributes the training across processes with DDP; each process
picks its GPU from `LOCAL_RANK` automatically:

```bash
./build/infini_run \
--nnodes=1 \
--nproc_per_node=2 \
./build/mnist \
--model cnn \
--device cuda \
--dataset data/mnist \
--num_epoch 3
```

##### GPT-2 124M

```bash
Expand Down
44 changes: 44 additions & 0 deletions example/mnist/cnn_net.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <memory>
#include <utility>
#include <vector>

#include "glog/logging.h"

#include "infini_train/include/nn/modules/activations.h"
#include "infini_train/include/nn/modules/container.h"
#include "infini_train/include/nn/modules/conv.h"
#include "infini_train/include/nn/modules/linear.h"
#include "infini_train/include/nn/modules/module.h"
#include "infini_train/include/tensor.h"

// Small CNN classifier for the MNIST demo. Structure matches the reference:
// Conv2d(1,16,3) -> ReLU -> Conv2d(16,32,3) -> ReLU -> Flatten -> Linear(18432,10).
// The DataLoader::Stack helper flattens each image into a [N, 784] matrix, so the
// Forward entry restores the (N, 1, 28, 28) spatial layout before the first conv.
class MnistCnn : public infini_train::nn::Module {
public:
MnistCnn() {
std::vector<std::shared_ptr<infini_train::nn::Module>> layers;
// Two 3x3 valid convs shrink 28 -> 26 -> 24; no pooling in the reference net.
layers.push_back(std::make_shared<infini_train::nn::Conv2d>(1, 16, 3));
layers.push_back(std::make_shared<infini_train::nn::ReLU>());
layers.push_back(std::make_shared<infini_train::nn::Conv2d>(16, 32, 3));
layers.push_back(std::make_shared<infini_train::nn::ReLU>());
modules_["sequential"] = std::make_shared<infini_train::nn::Sequential>(std::move(layers));
// 32 * 24 * 24 = 18432 flattened features into the 10-class head.
modules_["linear"] = std::make_shared<infini_train::nn::Linear>(32 * 24 * 24, 10);
}

std::vector<std::shared_ptr<infini_train::Tensor>>
Forward(const std::vector<std::shared_ptr<infini_train::Tensor>> &x) override {
CHECK_EQ(x.size(), 1);
// Restore the batch dimension from the runtime shape, then reshape the flattened
// [N, 784] input to (N, 1, 28, 28). View is element-order preserving.
const auto batch = x[0]->Dims()[0];
auto x_view = x[0]->View({batch, 1, 28, 28});
auto x_feat = (*modules_["sequential"])({x_view})[0]->Flatten(1, -1);
return (*modules_["linear"])({x_feat});
}
};
7 changes: 6 additions & 1 deletion example/mnist/dataset.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,12 @@ MNISTDataset::MNISTDataset(const std::string &dataset, bool train)
std::format("{}/{}-labels-idx1-ubyte", dataset, train ? kTrainPrefix : kTestPrefix))),
image_dims_(image_file_.dims.begin() + 1, image_file_.dims.end()),
label_dims_(label_file_.dims.begin() + 1, label_file_.dims.end()),
image_size_in_bytes_(kSN3TypeToSize.at(image_file_.type)
// The image tensor is normalized to FLOAT32 in the constructor body, so the per-sample
// byte stride must use the float element size, not the on-disk UINT8 size. After the
// first sample, the UINT8 stride reads the wrong bytes: most views are a misaligned
// mix of real pixels, and some coincide with another sample's exact copy but are still
// paired with an unrelated label.
image_size_in_bytes_(infini_train::kDataTypeToSize.at(DataType::kFLOAT32)
* std::accumulate(image_dims_.begin(), image_dims_.end(), 1, std::multiplies<int>())),
label_size_in_bytes_(kSN3TypeToSize.at(label_file_.type)
* std::accumulate(label_dims_.begin(), label_dims_.end(), 1, std::multiplies<int>())) {
Expand Down
116 changes: 100 additions & 16 deletions example/mnist/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <iostream>
#include <memory>
#include <numeric>
#include <optional>
#include <vector>

#include "gflags/gflags.h"
Expand All @@ -12,15 +13,27 @@
#include "infini_train/include/dataloader.h"
#include "infini_train/include/device.h"
#include "infini_train/include/nn/modules/loss.h"
#include "infini_train/include/nn/parallel/ddp/distributed_data_parallel.h"
#include "infini_train/include/nn/parallel/ddp/distributed_data_parallel_config.h"
#include "infini_train/include/nn/parallel/global.h"
#include "infini_train/include/nn/parallel/parallel_functional.h"
#include "infini_train/include/nn/parallel/process_group.h"
#include "infini_train/include/nn/parallel/rank.h"
#include "infini_train/include/nn/parallel/utils.h"
#include "infini_train/include/optimizer.h"
#include "infini_train/include/tensor.h"

#include "example/mnist/cnn_net.h"
#include "example/mnist/dataset.h"
#include "example/mnist/net.h"

DEFINE_string(dataset, "", "mnist dataset path");
DEFINE_string(model, "cnn", "model type (mlp/cnn)");
DEFINE_int32(bs, 64, "batch size");
DEFINE_int32(num_epoch, 1, "num epochs");
DEFINE_double(lr, 0.01, "learning rate");
// Defaults are tuned for the CNN demo (default --model=cnn) to reach ~97.8% test accuracy.
// The MLP reaches ~92% at these defaults; pass more epochs (e.g. --num_epoch=20) to reach ~95%.
DEFINE_int32(num_epoch, 3, "num epochs");
DEFINE_double(lr, 0.1, "learning rate");
DEFINE_string(device, "cpu", "device type (cpu/cuda)");

using namespace infini_train;
Expand All @@ -31,55 +44,126 @@ constexpr int kNumClasses = 10;

constexpr char kDeviceCPU[] = "cpu";
constexpr char kDeviceCUDA[] = "cuda";
constexpr char kModelMLP[] = "mlp";
constexpr char kModelCNN[] = "cnn";
}; // namespace

DEFINE_validator(device,
[](const char *, const std::string &value) { return value == kDeviceCPU || value == kDeviceCUDA; });
DEFINE_validator(model,
[](const char *, const std::string &value) { return value == kModelMLP || value == kModelCNN; });

int main(int argc, char *argv[]) {
gflags::ParseCommandLineFlags(&argc, &argv, true);
google::InitGoogleLogging(argv[0]);

CHECK_GT(FLAGS_bs, 0) << "--bs must be a positive batch size (got " << FLAGS_bs << ")";

// Consume the WORLD_SIZE/RANK/LOCAL_RANK env injected by infini_run; without the launcher
// every size defaults to 1 and the single-process path below is unchanged.
nn::parallel::global::InitAllEnv(/*nthread_per_process=*/1, /*tensor_parallel_size=*/1,
/*sequence_parallel_enabled=*/false, /*pipeline_parallel_size=*/1,
/*virtual_pipeline_parallel_size=*/1);
nn::parallel::Rank rank(nn::parallel::global::GetGlobalProcRank(), /*thread_rank=*/0,
nn::parallel::global::GetNprocPerNode(), nn::parallel::global::GetNthreadPerProc());
const int ddp_world_size = nn::parallel::global::GetDataParallelSize();

auto train_dataset = std::make_shared<MNISTDataset>(FLAGS_dataset, true);
DataLoader train_dataloader(train_dataset, FLAGS_bs);

Device device;
const nn::parallel::ProcessGroup *ddp_pg = nullptr;
int ddp_rank = 0;
if (rank.IsParallel()) {
CHECK_EQ(FLAGS_device, kDeviceCUDA) << "Distributed training requires --device=cuda";
// One GPU per process, taken from the LOCAL_RANK assigned by the launcher.
device = Device(Device::DeviceType::kCUDA, nn::parallel::global::GetDeviceIndex(rank.thread_rank()));
auto *pg_factory = nn::parallel::ProcessGroupFactory::Instance(device.type());
ddp_pg = pg_factory->GetOrCreate(nn::parallel::GetDataParallelProcessGroupName(rank.GlobalRank()),
nn::parallel::GetDataParallelGroupRanks(rank.GlobalRank()));
ddp_rank = ddp_pg->GetGroupRank(rank.GlobalRank());
} else {
device = FLAGS_device == kDeviceCPU ? Device() : Device(Device::DeviceType::kCUDA, 0);
}

// Sharded batches per rank under DDP; the legacy loader keeps the single-process path.
std::optional<DistributedDataLoader> ddp_train_dataloader;
std::optional<DataLoader> plain_train_dataloader;
if (ddp_pg != nullptr) {
// The loss AllReduce runs step-for-step on every rank, so all ranks must see the same
// batch count or a collective would hang out of step. DistributedDataLoader derives
// that count from the global batch size, which keeps every rank aligned.
ddp_train_dataloader.emplace(train_dataset, FLAGS_bs, ddp_rank, ddp_world_size);
} else {
plain_train_dataloader.emplace(train_dataset, FLAGS_bs);
}
const DataLoader &train_loader = ddp_pg != nullptr ? *ddp_train_dataloader : *plain_train_dataloader;

// TODO(dcj): Add sampler & eval dataloader later.
// The test loader stays unsharded so every rank reports metrics over the full test set.
auto test_dataset = std::make_shared<MNISTDataset>(FLAGS_dataset, false);
DataLoader test_dataloader(test_dataset, FLAGS_bs);

auto network = MNIST();
Device device = FLAGS_device == kDeviceCPU ? Device() : Device(Device::DeviceType::kCUDA, 0);
std::shared_ptr<nn::Module> network;
if (FLAGS_model == kModelCNN) {
network = std::make_shared<MnistCnn>();
} else {
network = std::make_shared<MNIST>();
}
Device cpu_device = Device();
network.To(device);
network->To(device);

auto loss_fn = std::make_shared<nn::CrossEntropyLoss>();
loss_fn->To(device);

// Wrap with DDP only after all device conversions: a later .To() recreates parameter
// tensors and would leave the gradient hooks registered at wrap time dangling.
if (ddp_pg != nullptr) {
network = std::make_shared<nn::parallel::DistributedDataParallel>(
network, rank, nn::parallel::DistributedDataParallelConfig{});
// Keep every replica starting from the same state; parameter broadcast before training
// is part of the DDP contract and left to the caller by the wrapper. Parameters only:
// the demo network carries no buffers, which PyTorch would sync alongside them.
ddp_pg->Broadcast(network->Parameters(), /*root_rank_in_group=*/0);
}

auto loss_fn = nn::CrossEntropyLoss();
loss_fn.To(device);
auto optimizer = optimizers::SGD(network.Parameters(), FLAGS_lr);
auto optimizer = optimizers::SGD(network->Parameters(), FLAGS_lr);

for (int epoch = 0; epoch < FLAGS_num_epoch; ++epoch) {
int train_idx = 0;
float total_loss = 0.0;

const auto epoch_start = std::chrono::high_resolution_clock::now();

for (const auto &[image, label] : train_dataloader) {
for (const auto &[image, label] : train_loader) {
auto new_image = std::make_shared<Tensor>(image->To(device));
auto new_label = std::make_shared<Tensor>(label->To(device));

auto outputs = network.Forward({new_image});
// Zero grads before forward: DDP rebinds param.grad to its bucket view during forward.
optimizer.ZeroGrad();

auto loss = loss_fn.Forward({outputs[0], new_label});
auto outputs = (*network)({new_image});

auto loss = (*loss_fn)({outputs[0], new_label});
loss[0]->Backward();

// Defer the loss D2H copy until after backward; reading it earlier would synchronize CUDA
// between forward and backward.
auto loss_cpu = loss[0]->To(cpu_device);
float current_loss = static_cast<float *>(loss_cpu.DataPtr())[0];
if (ddp_pg != nullptr) {
// Average the per-rank loss so the logged value matches the global batch. With an
// uneven trailing batch the equal-weight average is a bounded per-step approximation
// until a sampler lands.
auto loss_stat
= std::make_shared<Tensor>(&current_loss, std::vector<int64_t>{}, DataType::kFLOAT32, device);
nn::parallel::function::AllReduce(loss_stat, nn::parallel::function::ReduceOpType::kAvg, ddp_pg);
auto loss_stat_cpu = loss_stat->To(cpu_device);
current_loss = static_cast<const float *>(loss_stat_cpu.DataPtr())[0];
}
total_loss += current_loss;
if (train_idx % kNumItersOfOutputDuration == 0) {
LOG(ERROR) << "epoch: " << epoch << ", [" << train_idx * FLAGS_bs << "/" << train_dataset->Size()
<< "] "
LOG(ERROR) << "epoch: " << epoch << ", [" << train_idx * FLAGS_bs * ddp_world_size << "/"
<< train_dataset->Size() << "] "
<< " loss: " << current_loss;
}

Expand All @@ -104,9 +188,9 @@ int main(int argc, char *argv[]) {
auto new_label = std::make_shared<Tensor>(label->To(device));

auto label_cpu = label->To(cpu_device);
auto outputs = network.Forward({new_image});
auto outputs = (*network)({new_image});
auto output_cpu = outputs[0]->To(cpu_device);
auto loss = loss_fn.Forward({outputs[0], new_label});
auto loss = (*loss_fn)({outputs[0], new_label});
auto loss_cpu = loss[0]->To(cpu_device);

const int batch_size = output_cpu.Dims()[0];
Expand Down
12 changes: 12 additions & 0 deletions infini_train/include/autograd/activations.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,16 @@ class Sigmoid : public Function {
const std::vector<std::shared_ptr<Tensor>> &output_tensors) override;
std::vector<std::shared_ptr<Tensor>> Backward(const std::vector<std::shared_ptr<Tensor>> &grad_outputs) override;
};

class ReLU : public Function {
public:
static constexpr char kType[] = "ReLUFunction";

ReLU() : Function(kType) {}

std::vector<std::shared_ptr<Tensor>> Forward(const std::vector<std::shared_ptr<Tensor>> &input_tensors) override;
void SetupContext(const std::vector<std::shared_ptr<Tensor>> &input_tensors,
const std::vector<std::shared_ptr<Tensor>> &output_tensors) override;
std::vector<std::shared_ptr<Tensor>> Backward(const std::vector<std::shared_ptr<Tensor>> &grad_outputs) override;
};
} // namespace infini_train::autograd
31 changes: 31 additions & 0 deletions infini_train/include/autograd/conv.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once

#include <cstdint>
#include <memory>
#include <vector>

#include "infini_train/include/autograd/function.h"

namespace infini_train {
class Tensor;
}

namespace infini_train::autograd {

class Conv2d : public Function {
public:
static constexpr char kType[] = "Conv2dFunction";

Conv2d() : Function(kType) {}

std::vector<std::shared_ptr<Tensor>> Forward(const std::vector<std::shared_ptr<Tensor>> &input_tensors) override;
void SetupContext(const std::vector<std::shared_ptr<Tensor>> &input_tensors,
const std::vector<std::shared_ptr<Tensor>> &output_tensors) override;
std::vector<std::shared_ptr<Tensor>> Backward(const std::vector<std::shared_ptr<Tensor>> &grad_outputs) override;

private:
bool bias_ = false;
int64_t out_channels_ = 0;
std::vector<int64_t> input_dims_;
};
} // namespace infini_train::autograd
7 changes: 7 additions & 0 deletions infini_train/include/nn/modules/activations.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class Sigmoid : public CloneableModule<Sigmoid> {
std::vector<std::shared_ptr<Tensor>> Forward(const std::vector<std::shared_ptr<Tensor>> &input_tensors) override;
};

class ReLU : public CloneableModule<ReLU> {
public:
static constexpr char kType[] = "ReLU";
ReLU() : CloneableModule(kType) {}
std::vector<std::shared_ptr<Tensor>> Forward(const std::vector<std::shared_ptr<Tensor>> &input_tensors) override;
};

class NewGELU : public CloneableModule<NewGELU> {
public:
static constexpr char kType[] = "NewGELU";
Expand Down
31 changes: 31 additions & 0 deletions infini_train/include/nn/modules/conv.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once

#include <memory>
#include <vector>

#include "infini_train/include/device.h"
#include "infini_train/include/nn/modules/module.h"

namespace infini_train {
class Tensor;
class Device;
} // namespace infini_train

namespace infini_train::nn {
class Conv2d : public CloneableModule<Conv2d> {
public:
static constexpr char kType[] = "Conv2d";

static constexpr char kParamWeightName[] = "weight";
static constexpr char kParamBiasName[] = "bias";

Conv2d(int64_t in_channels, int64_t out_channels, int64_t kernel_size, bool bias = true, Device device = Device());
std::vector<std::shared_ptr<Tensor>> Forward(const std::vector<std::shared_ptr<Tensor>> &input_tensors) override;

bool has_bias() const { return bias_; }

private:
void ResetParameters();
bool bias_ = true;
};
} // namespace infini_train::nn
25 changes: 25 additions & 0 deletions infini_train/src/autograd/activations.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,29 @@ std::vector<std::shared_ptr<Tensor>> Sigmoid::Backward(const std::vector<std::sh
auto device = output->GetDevice().type();
return {Dispatcher::Instance().Call<std::shared_ptr<Tensor>>({device, "SigmoidBackward"}, output, grad_output)};
}

std::vector<std::shared_ptr<Tensor>> ReLU::Forward(const std::vector<std::shared_ptr<Tensor>> &input_tensors) {
CHECK_EQ(input_tensors.size(), 1);
const auto &input = input_tensors[0];

auto device = input->GetDevice().type();
return {Dispatcher::Instance().Call<std::shared_ptr<Tensor>>({device, "ReLUForward"}, input)};
}

void ReLU::SetupContext(const std::vector<std::shared_ptr<Tensor>> &input_tensors,
const std::vector<std::shared_ptr<Tensor>> &) {
// ReLU backward needs the input activations to decide where the gradient flows.
ctx_.SaveForBackward({input_tensors[0]});
}

std::vector<std::shared_ptr<Tensor>> ReLU::Backward(const std::vector<std::shared_ptr<Tensor>> &grad_outputs) {
auto saved_tensors = ctx_.GetSavedTensors();
CHECK_EQ(saved_tensors.size(), 1);
const auto &input = saved_tensors[0];
CHECK_EQ(grad_outputs.size(), 1);
const auto &grad_output = grad_outputs[0];

auto device = input->GetDevice().type();
return {Dispatcher::Instance().Call<std::shared_ptr<Tensor>>({device, "ReLUBackward"}, input, grad_output)};
}
} // namespace infini_train::autograd
Loading