|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * All rights reserved. |
| 4 | + * |
| 5 | + * This source code is licensed under the BSD-style license found in the |
| 6 | + * LICENSE file in the root directory of this source tree. |
| 7 | + */ |
| 8 | + |
| 9 | +#include <gtest/gtest.h> |
| 10 | + |
| 11 | +#include <ATen/ATen.h> |
| 12 | + |
| 13 | +#include <executorch/backends/vulkan/runtime/api/api.h> |
| 14 | +#include <executorch/backends/vulkan/runtime/graph/ComputeGraph.h> |
| 15 | +#include <executorch/backends/vulkan/runtime/graph/ops/OperatorRegistry.h> |
| 16 | + |
| 17 | +#include "test_utils.h" |
| 18 | + |
| 19 | +#include <iostream> |
| 20 | +#include <vector> |
| 21 | + |
| 22 | +// |
| 23 | +// Reference implementation: transcription of the CPU-eager fused_ce_impl |
| 24 | +// (backends/vulkan/custom_ops_lib.py), computed with ATen (library golden). |
| 25 | +// |
| 26 | + |
| 27 | +std::pair<at::Tensor, at::Tensor> fused_ce_reference( |
| 28 | + const at::Tensor& logits, |
| 29 | + const at::Tensor& labels, |
| 30 | + double n_valid) { |
| 31 | + at::Tensor mask = labels.ge(0); |
| 32 | + at::Tensor safe = labels.clamp_min(0).to(at::kLong); |
| 33 | + at::Tensor lse = at::logsumexp(logits, -1); |
| 34 | + at::Tensor picked = logits.gather(-1, safe.unsqueeze(-1)).squeeze(-1); |
| 35 | + at::Tensor loss = |
| 36 | + at::where(mask, (lse - picked) / n_valid, at::zeros_like(lse)).sum(); |
| 37 | + at::Tensor softmax = at::softmax(logits, -1); |
| 38 | + at::Tensor onehot = at::one_hot(safe, logits.size(-1)).to(logits.dtype()); |
| 39 | + at::Tensor dlogits = at::where( |
| 40 | + mask.unsqueeze(-1), |
| 41 | + (softmax - onehot) / n_valid, |
| 42 | + at::zeros_like(softmax)); |
| 43 | + return {loss, dlogits}; |
| 44 | +} |
| 45 | + |
| 46 | +void test_vulkan_fused_ce( |
| 47 | + const int n_rows, |
| 48 | + const int vocab, |
| 49 | + const std::vector<int32_t>& labels_data, |
| 50 | + const double n_valid) { |
| 51 | + at::manual_seed(0); |
| 52 | + |
| 53 | + at::Tensor logits = |
| 54 | + at::rand({n_rows, vocab}, at::device(at::kCPU).dtype(at::kFloat)) * 4.0 - |
| 55 | + 2.0; |
| 56 | + at::Tensor labels = |
| 57 | + at::from_blob( |
| 58 | + const_cast<int32_t*>(labels_data.data()), {n_rows}, at::kInt) |
| 59 | + .clone(); |
| 60 | + |
| 61 | + auto ref = fused_ce_reference(logits, labels, n_valid); |
| 62 | + at::Tensor ref_loss = ref.first; |
| 63 | + at::Tensor ref_dlogits = ref.second; |
| 64 | + |
| 65 | + using namespace vkcompute; |
| 66 | + |
| 67 | + GraphConfig config; |
| 68 | + ComputeGraph graph(config); |
| 69 | + |
| 70 | + IOValueRef r_logits = graph.add_input_tensor( |
| 71 | + logits.sizes().vec(), vkapi::kFloat, utils::kBuffer); |
| 72 | + IOValueRef r_labels = |
| 73 | + graph.add_input_tensor(labels.sizes().vec(), vkapi::kInt, utils::kBuffer); |
| 74 | + |
| 75 | + const ValueRef r_n_valid = graph.add_scalar<double>(n_valid); |
| 76 | + |
| 77 | + const ValueRef r_loss = graph.add_tensor({}, vkapi::kFloat, utils::kBuffer); |
| 78 | + const ValueRef r_dlogits = |
| 79 | + graph.add_tensor({n_rows, vocab}, vkapi::kFloat, utils::kBuffer); |
| 80 | + const ValueRef r_out = graph.add_value_list({r_loss, r_dlogits}); |
| 81 | + |
| 82 | + VK_GET_OP_FN("et_vk.fused_ce.default") |
| 83 | + (graph, {r_logits.value, r_labels.value, r_n_valid, r_out}); |
| 84 | + |
| 85 | + ValueRef staging_loss = graph.set_output_tensor(r_loss); |
| 86 | + ValueRef staging_dlogits = graph.set_output_tensor(r_dlogits); |
| 87 | + |
| 88 | + graph.prepare(); |
| 89 | + graph.prepack(); |
| 90 | + graph.propagate_resize(); |
| 91 | + |
| 92 | + graph.maybe_cast_and_copy_into_staging( |
| 93 | + r_logits.staging, logits.const_data_ptr(), logits.numel(), vkapi::kFloat); |
| 94 | + graph.maybe_cast_and_copy_into_staging( |
| 95 | + r_labels.staging, labels.const_data_ptr(), labels.numel(), vkapi::kInt); |
| 96 | + |
| 97 | + graph.execute(); |
| 98 | + |
| 99 | + at::Tensor vk_loss = at::zeros({}, at::device(at::kCPU).dtype(at::kFloat)); |
| 100 | + graph.maybe_cast_and_copy_from_staging( |
| 101 | + staging_loss, vk_loss.mutable_data_ptr(), 1, vkapi::kFloat); |
| 102 | + |
| 103 | + at::Tensor vk_dlogits = |
| 104 | + at::zeros({n_rows, vocab}, at::device(at::kCPU).dtype(at::kFloat)); |
| 105 | + graph.maybe_cast_and_copy_from_staging( |
| 106 | + staging_dlogits, |
| 107 | + vk_dlogits.mutable_data_ptr(), |
| 108 | + vk_dlogits.numel(), |
| 109 | + vkapi::kFloat); |
| 110 | + |
| 111 | + const double atol = 1e-4; |
| 112 | + const double rtol = 1e-4; |
| 113 | + |
| 114 | + const bool loss_ok = at::allclose(ref_loss, vk_loss, rtol, atol); |
| 115 | + const bool dlogits_ok = at::allclose(ref_dlogits, vk_dlogits, rtol, atol); |
| 116 | + |
| 117 | + if (!loss_ok || !dlogits_ok) { |
| 118 | + std::cout << "fused_ce mismatch: n_rows=" << n_rows << " vocab=" << vocab |
| 119 | + << " n_valid=" << n_valid << std::endl; |
| 120 | + std::cout << "loss ref=" << ref_loss.item<float>() |
| 121 | + << " vk=" << vk_loss.item<float>() << std::endl; |
| 122 | + std::cout << "max dlogits diff=" |
| 123 | + << at::max(at::abs(ref_dlogits - vk_dlogits)).item<float>() |
| 124 | + << std::endl; |
| 125 | + } |
| 126 | + ASSERT_TRUE(loss_ok); |
| 127 | + ASSERT_TRUE(dlogits_ok); |
| 128 | +} |
| 129 | + |
| 130 | +TEST(VulkanFusedCeTest, all_valid_small) { |
| 131 | + test_vulkan_fused_ce( |
| 132 | + /*n_rows=*/4, /*vocab=*/8, /*labels=*/{0, 3, 7, 1}, /*n_valid=*/4.0); |
| 133 | +} |
| 134 | + |
| 135 | +TEST(VulkanFusedCeTest, masked_label) { |
| 136 | + // One masked row (label < 0): contributes 0 to loss and 0 gradient. |
| 137 | + test_vulkan_fused_ce( |
| 138 | + /*n_rows=*/4, /*vocab=*/8, /*labels=*/{2, -1, 5, 0}, /*n_valid=*/3.0); |
| 139 | +} |
| 140 | + |
| 141 | +TEST(VulkanFusedCeTest, large_vocab_strided_reduce) { |
| 142 | + // vocab >> NWORKERS exercises the strided per-row reduction. |
| 143 | + test_vulkan_fused_ce( |
| 144 | + /*n_rows=*/3, /*vocab=*/200, /*labels=*/{17, -1, 199}, /*n_valid=*/2.0); |
| 145 | +} |
0 commit comments