Files
albanD f6a98bbf71 [RAWTHROW][6] test/cpp and test/cpp_extensions: 43 conversions, 5 that must not be (#194773)
## Author Notes

Pretty mechanical, nothing special here.

## Agent Notes

Written by Claude Code:

> The two test directories are 48 findings, and they come apart cleanly: 43 are
> stubs and error checks that convert mechanically, and 5 are throws whose type is
> the point of the test they sit in. Getting those 5 wrong would leave the suite
> green while testing the wrong thing, which is the only real risk in this PR.
>
> The 43: `rng_extension.cpp` and `cpp_c10d_extension.cpp` are "not implemented" /
> "does not support" overrides (8 and 10); `aoti_inference/test.cpp` is CUDA API
> error checks (16); the rest are `aoti_custom_class.cpp`,
> `test_custom_class_registrations.cpp` (6, a toy stack machine's error paths) and
> the two `my_from_blob_*` extensions. None of their messages is asserted on
> anywhere - I grepped the tree for each, including the Python suites.
>
> The two `my_from_blob_*` sites get STD_TORCH_CHECK rather than TORCH_CHECK.
> Those TUs include only `torch/csrc/stable/*`, which reaches
> torch/headeronly/util/Exception.h but never c10/util/Exception.h, so TORCH_CHECK
> is not declared there at all. STD_TORCH_CHECK preserves the exception type
> exactly - it throws std::runtime_error, same as the raw throw - but not the
> message: like the serialization-codegen PR earlier in this stack, it prepends
> __func__, __FILE__ and __LINE__, so
>
>     before:  cudaMalloc failed
>     after:   my_from_blob_with_cuda_deleter, <path>/my_from_blob_with_deleter.cpp:73,
>              cudaMalloc failed
>
> These are the only two sites in this PR where anything observable moves. Nothing
> asserts on either string, both are behind #ifdef LAE_USE_CUDA in a test
> extension, and STD_TORCH_CHECK is already the house style in the sibling
> libtorch_agn_2_* extensions (17 uses), so their messages are decorated the same
> way. The alternative was to leave these two raw and annotated; taking the
> decoration keeps the file consistent with its neighbours.
>
> The 5 that stay raw, with the reason on each site:
>
>     ProcessGroupNCCLErrorsTest.cpp   TestHeartbeatMonitor::runLoop, 16 lines
>                                      above, catches std::runtime_error by name
>                                      to set hasMonitorThreadCaughtError_. A
>                                      c10::Error is not caught by that, and the
>                                      throw happens on the monitor thread.
>     dataloader.cpp                   The test rethrows e.original_exception and
>                                      asserts it is still a std::invalid_argument
>                                      - the point being that WorkerException
>                                      round-trips the original type.
>     parallel.cpp                     ParallelApplyRethrowsException is about a
>                                      user exception escaping forward().
>     FileStoreTest.cpp                std::system_error carrying errno as a
>                                      std::error_code.
>     OpenRegException.cpp             Reports a caller-supplied SourceLocation;
>                                      a TORCH_CHECK there would stamp this
>                                      file's own location instead.
>
> Two of those five are load-bearing in a way I could measure rather than argue,
> and the test plan shows the failures. `parallel.cpp` is the one that is not:
> its assertion is `ASSERT_THROWS_WITH(..., "Badness!")`, a substring match that
> would still pass after conversion. It is annotated for intent, not because it
> would break, and it seemed worth being explicit about that difference rather
> than implying all five were equally forced.
>
> The exclude list goes from 11 entries to 9.
>
> Test Plan:
>
> Half of this is CUDA or distributed and does not build here, so the two CUDA
> files - parallel.cpp and ProcessGroupNCCLErrorsTest.cpp - deliberately received
> comment-only edits, which is the safest possible change to code a configuration
> cannot compile.
>
> For the rest, what matters is whether the *changed lines* were compiled, not the
> files: three of them sit behind preprocessor guards that a plain CPU build skips
> straight over. `aoti_inference/test.cpp`'s 16 conversions are inside
> `#if defined(USE_CUDA) || defined(USE_ROCM)`, and the two `my_from_blob_*` sites
> are inside `#ifdef LAE_USE_CUDA`, which setup.py only defines when CUDA is
> available. The two my_from_blob sites were compiled with the guard forced on,
> against a stub cuda_runtime.h. There is no CUDA toolkit here, so the
> aoti_inference block was not compiled; what I checked instead is that
> `<torch/torch.h>` is included unconditionally above it and does provide
> TORCH_CHECK, and that the block contained no TORCH_CHECK before this change, so
> the macro's availability there is an argument rather than a measurement. Every
> other converted line is unguarded.
>
> BUILD_TEST=0 does not prevent compiling these directly:
>
>     aoti_inference/test.cpp                  syntax-only (needs gmock headers)
>     aoti_custom_class.cpp                    syntax-only
>     dataloader.cpp                           87 tests passed
>     test_custom_class_registrations.cpp      compiles and links (no TESTs of its own)
>     FileStoreTest.cpp                        syntax-only
>     cpp_c10d_extension.cpp                   syntax-only (needs Python.h, pybind11)
>     rng_extension.cpp                        syntax-only (same)
>     my_from_blob_with_deleter.cpp            syntax-only
>     my_from_blob_with_lambda_deleter.cpp     syntax-only
>     OpenRegException.cpp                     syntax-only (needs the openreg headers)
>
> ```
> g++ -std=c++20 <includes> test/cpp/api/dataloader.cpp gtest-all.o gtest_main.o \
>     -Lbuild/lib -lc10 -ltorch_cpu -lpthread && ./a.out
>     [  PASSED  ] 87 tests.
> ```
>
> Positive control on the dataloader annotation - the same file with the throw
> converted:
>
> ```
>     [  FAILED  ] DataLoaderTest.TestExceptionsArePropagatedFromWorkers
> ```
>
> So that annotation is required, not preference.
>
> ```
> lintrunner --take RAWTHROW --all-files
> spin lint
> ```
>
> Both clean; test/cpp and test/cpp_extensions go from 48 findings to 0.
> CLANGFORMAT rewrapped three of the new comments, applied via `spin fixlint`.
>
> One scope caveat worth stating plainly: "48 findings to 0" is about the linter's
> view, not the directories. RAWTHROW's include_patterns are `**/*.cpp` and
> `**/*.h`, so four raw throws here stay invisible - three std::system_error sites
> in `test/cpp/c10d/TestUtils.hpp` (two of which are literal copies of the throw
> this PR annotates in FileStoreTest.cpp) and a CUDA error-check macro in
> `test/cpp/aoti_abi_check/cuda/test_kernel_utils.cu`. A planned later phase
> widens include_patterns to .hpp/.cu and picks them up.
>
> One coupling to carry forward, in a file this PR touches:
> `aoti_inference/test.cpp:301` catches `const std::runtime_error&` around
> `update_constant_buffer` and matches "API call failed at" at :302. That throw comes from
> `torch/headeronly/util/shim_utils.h`, which is annotated and unconverted today -
> if a later phase converts it to TORCH_CHECK rather than STD_TORCH_CHECK, this
> catch stops matching.
>
> Not verified locally: the CUDA and distributed builds, and CLANGTIDY.
Pull Request resolved: https://github.com/pytorch/pytorch/pull/194773
Approved by: https://github.com/ezyang
ghstack dependencies: #194379, #194772
2026-08-31 23:48:04 +00:00

298 lines
9.5 KiB
C++

#include <gtest/gtest.h>
#include <c10/util/irange.h>
#include <torch/csrc/autograd/functions/comm.h>
#include <torch/nn/module.h>
#include <torch/nn/modules/conv.h>
#include <torch/nn/modules/linear.h>
#include <torch/nn/parallel/data_parallel.h>
#include <torch/nn/pimpl.h>
#include <torch/optim/sgd.h>
#include <torch/types.h>
#include <torch/utils.h>
#include <test/cpp/api/support.h>
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
using namespace torch::autograd;
using namespace torch::nn;
struct ParallelTest : torch::test::SeedingFixture {};
TEST_F(ParallelTest, DifferentiableScatter_MultiCUDA) {
Scatter scatter(
{torch::Device(torch::kCUDA, 0), torch::Device(torch::kCUDA, 1)});
auto input = torch::ones(10, torch::requires_grad(true));
auto output = scatter.apply({input});
ASSERT_EQ(output.size(), 2);
ASSERT_EQ(output[0].size(0), 5);
ASSERT_EQ(output[1].size(0), 5);
ASSERT_TRUE(torch::cat({output[0].to(torch::kCPU), output[1].to(torch::kCPU)})
.allclose(input));
torch::Tensor sum = output[0].to({torch::kCUDA, 1}) + output[1];
sum.backward(torch::ones_like(sum));
ASSERT_TRUE(input.grad().defined());
ASSERT_TRUE(input.grad().device().is_cpu());
ASSERT_EQ(input.grad().sum().item<int32_t>(), 10);
}
TEST_F(ParallelTest, DifferentiableGather_MultiCUDA) {
Gather gather(torch::Device(torch::kCUDA, 1));
auto a = torch::ones(5, torch::requires_grad(true).device(torch::kCUDA, 0));
auto b = torch::ones(5, torch::requires_grad(true).device(torch::kCUDA, 1));
auto outputs = gather.apply({a, b});
ASSERT_EQ(outputs.size(), 1);
torch::Tensor output = outputs.front();
ASSERT_EQ(output.size(0), 10);
ASSERT_EQ(output.device(), torch::Device(torch::kCUDA, 1));
auto chunks = output.chunk(2);
ASSERT_TRUE(chunks[0].to({torch::kCUDA, 0}).allclose(a));
ASSERT_TRUE(chunks[1].allclose(b));
output.backward(torch::ones_like(output));
ASSERT_TRUE(a.grad().defined());
ASSERT_EQ(a.grad().device(), torch::Device(torch::kCUDA, 0));
ASSERT_EQ(a.grad().sum().item<int32_t>(), 5);
ASSERT_TRUE(b.grad().defined());
ASSERT_EQ(b.grad().device(), torch::Device(torch::kCUDA, 1));
ASSERT_EQ(b.grad().sum().item<int32_t>(), 5);
}
TEST_F(ParallelTest, Replicate_MultiCUDA) {
Linear linear(3, 4);
auto replicas = parallel::replicate(
linear, {torch::Device(torch::kCUDA, 0), torch::Device(torch::kCUDA, 1)});
ASSERT_EQ(replicas.size(), 2);
auto original_parameters = linear->parameters();
auto replica1_parameters = replicas[0]->parameters();
for (auto& parameter : replica1_parameters) {
ASSERT_EQ(parameter.device(), torch::Device(torch::kCUDA, 0));
}
replicas[0]->to(torch::kCPU);
ASSERT_EQ(replica1_parameters.size(), original_parameters.size());
for (const auto i : c10::irange(original_parameters.size())) {
ASSERT_TRUE(replica1_parameters[i].allclose(original_parameters[i]));
ASSERT_TRUE(
replica1_parameters[i].data_ptr<float>() !=
original_parameters[i].data_ptr<float>());
}
auto replica2_parameters = replicas[1]->parameters();
for (auto& parameter : replica2_parameters) {
ASSERT_EQ(parameter.device(), torch::Device(torch::kCUDA, 1));
}
replicas[1]->to(torch::kCPU);
ASSERT_EQ(replica2_parameters.size(), original_parameters.size());
for (const auto i : c10::irange(original_parameters.size())) {
ASSERT_TRUE(replica2_parameters[i].allclose(original_parameters[i]));
ASSERT_TRUE(
replica2_parameters[i].data_ptr<float>() !=
original_parameters[i].data_ptr<float>());
}
}
TEST_F(ParallelTest, ParallelApply_MultiCUDA) {
Linear a(3, 4);
Linear b(std::dynamic_pointer_cast<LinearImpl>(a->clone()));
b->to({torch::kCUDA, 0});
Linear c(std::dynamic_pointer_cast<LinearImpl>(a->clone()));
c->to({torch::kCUDA, 1});
std::vector<Linear> modules = {a, b, c};
std::vector<torch::Tensor> inputs = {
torch::ones({2, 3}),
torch::ones({2, 3}, torch::device({torch::kCUDA, 0})),
torch::ones({2, 3}, torch::device({torch::kCUDA, 1}))};
auto outputs = parallel::parallel_apply(modules, inputs);
ASSERT_EQ(outputs.size(), 3);
ASSERT_TRUE(outputs[0].device().is_cpu());
ASSERT_EQ(outputs[1].device(), torch::Device(torch::kCUDA, 0));
ASSERT_TRUE(outputs[1].to(torch::kCPU).allclose(outputs[0]));
ASSERT_EQ(outputs[2].device(), torch::Device(torch::kCUDA, 1));
ASSERT_TRUE(outputs[2].to(torch::kCPU).allclose(outputs[0]));
}
TEST_F(ParallelTest, ParallelApplyWithDifferentOutputDevice_MultiCUDA) {
struct M : torch::nn::Module {
torch::Tensor forward(torch::Tensor input) {
return torch::ones(5, torch::kInt32);
}
};
std::vector<std::shared_ptr<M>> modules = {
std::make_shared<M>(), std::make_shared<M>(), std::make_shared<M>()};
std::vector<torch::Tensor> inputs = {
torch::empty({}), torch::empty({}), torch::empty({})};
std::vector<torch::Device> devices = {
{torch::kCUDA, 1}, {torch::kCUDA, 0}, {torch::kCPU}};
auto outputs = parallel::parallel_apply(modules, inputs, devices);
ASSERT_EQ(outputs.size(), 3);
ASSERT_TRUE(outputs[0].device().is_cuda());
ASSERT_EQ(outputs[0].device(), torch::Device(torch::kCUDA, 1));
ASSERT_TRUE(outputs[1].device().is_cuda());
ASSERT_EQ(outputs[1].device(), torch::Device(torch::kCUDA, 0));
ASSERT_TRUE(outputs[2].device().is_cpu());
}
TEST_F(ParallelTest, ParallelApplyRethrowsException_MultiCUDA) {
struct M : torch::nn::Cloneable<M> {
void reset() override {}
torch::Tensor forward(torch::Tensor input) {
// ParallelApplyRethrowsException is about a user exception propagating
// out of forward(), so this should stay a plain std:: exception.
// @allow-raw-throw: intentionally a plain std:: exception
throw std::runtime_error("Badness!");
}
};
auto m = std::make_shared<M>();
auto input = torch::ones({10, 3});
ASSERT_THROWS_WITH(parallel::data_parallel(m, input), "Badness!");
}
TEST_F(
ParallelTest,
DataParallelPlacesTheOutputOnTheRequestedDevice_MultiCUDA) {
struct M : torch::nn::Cloneable<M> {
void reset() override {}
torch::Tensor forward(torch::Tensor input) {
// The returned tensor should be on the output device.
return torch::ones(3);
}
};
auto m = std::make_shared<M>();
auto input = torch::ones({10, 3});
{
auto output = parallel::data_parallel(
m,
input,
/*devices=*/std::nullopt,
/*output_device=*/torch::Device(torch::kCUDA, 1));
ASSERT_TRUE(output.defined());
ASSERT_TRUE(output.device().is_cuda());
ASSERT_EQ(output.device().index(), 1);
}
{
// Verify for the single-device case (where we don't scatter/gather).
auto output = parallel::data_parallel(
m,
input,
/*devices=*/std::vector<torch::Device>{torch::Device(torch::kCUDA, 0)},
/*output_device=*/torch::Device(torch::kCUDA, 1));
ASSERT_TRUE(output.defined());
ASSERT_TRUE(output.device().is_cuda());
ASSERT_EQ(output.device().index(), 1);
}
}
TEST_F(ParallelTest, DataParallelUsesAllAvailableCUDADevices_CUDA) {
struct M : torch::nn::Cloneable<M> {
void reset() override {}
torch::Tensor forward(torch::Tensor input) {
return torch::tensor({input.device().index()});
}
};
auto m = std::make_shared<M>();
const auto device_count = torch::cuda::device_count();
auto input = torch::ones({std::max(10, int(2 * device_count)), 3});
auto output = parallel::data_parallel(m, input);
ASSERT_EQ(output.numel(), device_count);
for (const auto i : c10::irange(device_count)) {
ASSERT_EQ(output[i].item<int32_t>(), i);
}
}
TEST_F(ParallelTest, DataParallelNumericalEquivalence_MultiCUDA) {
struct M : torch::nn::Cloneable<M> {
M() {
reset();
}
void reset() override {
conv = register_module(
"conv",
torch::nn::Conv2d(torch::nn::Conv2dOptions(2, 2, /*kernel_size=*/2)));
fc = register_module("fc", torch::nn::Linear(8, 2));
}
torch::Tensor forward(torch::Tensor x) {
x = conv->forward(x);
x = torch::relu(x);
x = x.view({-1, 8});
x = fc->forward(x);
return torch::log_softmax(x, /*dim=*/1);
}
torch::nn::Conv2d conv{nullptr};
torch::nn::Linear fc{nullptr};
};
// prepare modules and inputs
auto input = torch::ones({16, 2, 3, 3});
auto input_dp = torch::ones({16, 2, 3, 3});
auto model = std::make_shared<M>();
auto model_dp = std::dynamic_pointer_cast<M>(model->clone());
// run 3 training iterations
for (const auto i : c10::irange(3)) {
input += i;
input_dp += i;
// non-parallel training
torch::optim::SGD optim(model->parameters(), torch::optim::SGDOptions(0.1));
auto output = model->forward(input);
auto loss = torch::mse_loss(output, torch::zeros_like(output));
loss.backward();
optim.step();
// data-parallel training
torch::optim::SGD optim_dp(
model_dp->parameters(), torch::optim::SGDOptions(0.1));
auto output_dp = parallel::data_parallel(model_dp, input_dp);
auto loss_dp = torch::mse_loss(output_dp, torch::zeros_like(output_dp));
loss_dp.backward();
optim_dp.step();
// make sure that weights are the same
model->to(torch::kCPU);
model_dp->to(torch::kCPU);
auto params = model->parameters();
auto params_dp = model_dp->parameters();
ASSERT_EQ(params.size(), params_dp.size());
for (auto it = params.begin(), it_dp = params_dp.begin();
it != params.end() && it_dp != params.end();
++it, ++it_dp) {
ASSERT_TRUE(torch::allclose(*it, *it_dp));
}
}
}