Привет, Хабр! Меня зовут Кирилл, я разработчик СХД в YADRO и ML-энтузиаст, автор книги «Hands-on Machine Learning with C++». Я заметил, что роль С/С++ в экосистеме машинного обучения трансформируется прямо сейчас. Чтобы понять, какое значение язык играет в развитии ML, мы поговорим о классическом применении C++ для ручной оптимизации вычислительных ядер. Затем разберемся, почему новый стандарт не закрепляет реализаций линейной алгебры, а отдает это на откуп поставщикам стандартной библиотеки и вендорам оборудования. И в завершение подумаем, как работать с «зоопарком реализаций», который из-за этого остается.
Традиционное разделение ролей: Python и C++
Исторически роли Python и C++ в машинном обучении разделены. Python выступает в качестве пользовательского интерфейса (API), на котором дата-сайентисты и ML-инженеры описывают модели и строят пайплайны для обучения и инференса. C++ образует ядро платформ: на нем реализуются сами вычисления, менеджмент памяти и низкоуровневые операции.

C++ выбирают, потому что этот язык обеспечивает прямой доступ к аппаратным ресурсам, полный контроль над памятью и возможности для тонкой оптимизации производительности. Кроме того, на C++ исторически написан огромный пласт высокооптимизированных математических библиотек, которые стали фундаментом для современных ML-платформ.
На прикладном уровне в ML доминирует Python. Он предоставляет интерфейс как для фреймворков общего назначения (PyTorch, TensorFlow, JAX, MindSpore), позволяющих обучать модели и выполнять инференс, так и для специализированных inference-движков (TensorRT, OpenVINO, NCNN). Последние обычно не поддерживают обучение, но предельно оптимизированы под конкретное железо для эффективного выполнения моделей.
Обе эти абстракции опираются на тензорные компиляторы и базовые математические библиотеки, реализующие интерфейс BLAS и LAPACK. В свою очередь, эти библиотеки базируются на эффективных вычислительных ядрах, написанных под конкретные аппаратные платформы (CPU, GPU, NPU, ASIC). Низкоуровневую реализацию можно условно разделить на две распространенные категории: векторизация под CPU и вычисления на GPU,

Низкоуровневая векторизация (CPU)
Чтобы написать эффективный код под CPU на C++, используем SIMD-инструкции различных вендоров: AVX-512 (Intel), NEON и SVE (ARM), RVV (RISC-V). Эти технологии активно развиваются, пополняются не только векторными, но и матричными инструкциями. Это позволяет писать эффективные вычислительные алгоритмы для CPU.
На самом низком уровне для работы с расширениями в C/C++ удобнее использовать интринсики компилятора — обертки над ассемблерными инструкциями. Но каждый вендор, о котором я писал выше, и даже каждое поколение инструкций могут предлагать свой уникальный API с разной длиной векторных регистров и другими особенностями. Так что поддержка всех целевых платформ превращается в трудоемкую задачу. Поэтому в C++ такие API часто оборачиваются в библиотеки, которые позволяют написать обобщенный низкоуровневый код для SIMD-вычислений один раз и скомпилировать его под разные платформы. Таких библиотек много — например, XSIMD, EVE, highway. А в коде это выглядит примерно так:
template <typename DataType> void simd_relu_fwd(DataType* HWY_RESTRICT output, const DataType* HWY_RESTRICT input, const size_t size, bool aligned) { using D = hn::ScalableTag<DataType>; constexpr D d; auto zero = hn::Zero(d); auto op = [&](const hn::Vec<D>& vec) { auto gt = hn::Gt(vec, zero); return hn::IfThenElse(gt, vec, zero); }; map(std::move(op), output, input, size, aligned); }
Это достаточно высокоуровневый код, он использует шаблонное метапрограммирование, которое позволяет автоматически определять доступную ширину регистра. Уже определены высокоуровневые операции — if-then-else и greater. Могут добавиться какие-то более сложные алгоритмы типа map. Можно применить эту операцию в виде лямбды к буферу данных. Он поделит это фрагменты для загрузки вектора.
Автовекторизация и поддержка SIMD в C++26
Помимо ручных оптимизаций, в C и C++ давно существует автовекторизация, когда компилятор самостоятельно распознает относительно простые циклы без сложных ветвлений и генерирует код с использованием SIMD-инструкций. Например, одно из основных условий применения автовекторизации — определение количества итераций или того, какому числу оно кратко. В таком случае вы получите ускорение автоматически.
Более того, в стандарте C++26 на уровне языка ввели нативную поддержку SIMD. Появились новые типы данных для представления векторных регистров std::simd и дополнительные операторы, а математические функции переопределяются для использования оптимизированных инструкций.
Дополнительно вводятся политики выполнения алгоритмов. Например, в std::transform можно указать политику std::execution::simd, что явно предписывает использовать реализацию алгоритма в STL, оптимизированную с применением SIMD-инструкций.
Рассмотрим пример:
#include <simd> #include <vector> #include <cmath> using float_v = std::simd<float, std::simd_abi::native<float>>; void process_data(std::vector<float>& data) { size_t i = 0; size_t size = data.size(); for (; i + float_v::size() <= size; i += float_v::size()) { float_v v; v.copy_from(&data[i], std::element_aligned); v = std::sin(v); v.copy_to(&data[i], std::element_aligned); } for (; i < size; ++i) { data[i] = std::sin(data[i]); } }
Мы видим новый заголовочный файл <simd>. Описываем вектор типа float_v, так что используем SIMD-регистр как набор данных типа float. Здесь используется std::simd_abi::native — значит, будет скомпилирована реализация, оптимальная для текущей платформы. Также можно автоматически выбирать целевую платформу.
У типа для работы с регистрами есть функции copy_ from, copy_to. То есть мы можем загрузить сразу целый вектор. Обратите внимание, что цикл for идет не по одному элементу, а с шагом, то есть с длиной типа данных, которыми мы заполняем регистр. Также видим в коде выше, что математические функции переопределяются: появляется синус, который уже будет использовать оптимизированные SIMD-инструкции. Другие функции, адаптированные для SIMD-типов, тоже вводятся.
Алгоритмы
Также вводятся политики выполнения алгоритмов. Например, в алгоритме std:ransform, который складывает два вектора, мы как политику выполнения можем указать std::execution::simd. И в данном случае компилятор тоже должен генерировать оптимизированный код с использованием SIMD-инструкций.
#include <algorithm> #include <execution> #include <vector> void vector_add(const std::vector<float>& a, const std::vector<float>& b, std::vector<float>& res) { std::transform(std::execution::simd, a.begin(), a.end(), b.begin(), res.begin(), [](float x, float y) { return x + y; }); }
Про это мы еще поговорим чуть позже.
C++ в GPU
На стороне GPU экосистема фрагментирована из-за большого количества вендоров. C++ представлен в виде специализированных диалектов:
CUDA в NVIDIA: диалект C++17 с полноценной поддержкой шаблонов и расширенной функциональностью для GPU.
ROCm/HIP в AMD: базируется на C++17, синтаксически максимально приближен к CUDA.
Ascend C в Huawei: диалект C++ также со своим компилятором.
Помимо проприетарных решений, есть и открытые стандарты. OpenCL и Vulkan через Compute Pipeline используют специфичные диалекты C, что делает программирование под них более сложным по сравнению с C++. Альтернативой выступает SYCL — открытый стандарт от Khronos Group, который позволяет писать на чистом C++ и компилировать код под CPU, GPU и NPU.
Особенности синтаксиса CUDA
Если рассматривать классический подход, вычислительное ядро на CUDA — это диалект C++, поддерживающий все современные возможности языка, включая стандарт C++17: классы, наследование и шаблонное метапрограммирование. Ниже — пример ядра CUDA:
template <typename T> class VectorAddFunctor { public: __host__ device VectorAddFunctor(T* c, const T* a, const T* b, int size) : c(c), a(a), b(b), N(size) {} __device__ forceinline void operator()() const { int idx = blockIdx.x blockDim.x + threadIdx.x; if (idx < N) c[idx] = a[idx] + b[idx]; } private: T c; const T* a; const T* b; int N; };
Ключевое отличие CUDA и OpenCL от более стандартизированных подходов (таких как SYCL или чистый C++) — в наличии специфических языковых расширений для конфигурации и запуска вычислительных ядер. В CUDA для этого используется уникальный синтаксис с тройными угловыми скобками <<< >>>, через который передаются параметры сетки и блоков, определяющие топологию выполнения задачи на графическом процессоре:
template <typename T> global void vectorAddKernel(VectorAddFunctor<T> f) { f(); } --------------- VectorAddFunctor<T> functor(c, a, b, N); int threadsPerBlock = 256; int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock; vectorAddKernel<<<blocksPerGrid, threadsPerBlock>>>(functor);
Это дополнительный синтаксис CUDA для запуска конкретного вычислительного ядра. Видно, что в качестве параметров можно передавать даже исполняемые объекты — функцию, которую нужно выполнить. А в угловых скобках создаются параметры запуска — параметры сетки.
Ответ NVIDIA на сложность CUDA
В ответ на сложность ручной низкоуровневой оптимизации появляются библиотеки высокого уровня, написанные на C++ (в частности, на диалекте CUDA для экосистемы NVIDIA). Яркий пример — библиотека CUTLASS (CUDA Templates for Linear Algebra Subroutines), которая представляет собой набор C++-шаблонов для программирования примитивов линейной алгебры.
Основное внимание в ней уделяется операциям обобщенного матричного умножения (GEMM) и их модификациям, которые составляют до 90% вычислительной нагрузки в задачах машинного обучения. Помимо этого, библиотека охватывает операции свертки, редукции и работу с графами, которые часто могут быть эффективно сведены к задачам линейной алгебры. Таким образом, CUTLASS покрывает критически важный вычислительный домен.
Об операциях матричного умножения GEMM у меня есть отдельная статья. Прочитайте, если хотите узнать, как устроены GEMM в С++.
Использование таких библиотек не делает процесс программирования тривиальным, но существенно повышает уровень абстракции. Разработчику больше не нужно вручную управлять регистрами, паттернами доступа к иерархии памяти и аппаратной спецификой. Вместо этого он может решать алгоритмические задачи: описывать тайлинг матричных блоков, их индексацию, распределять данные и выбирать стратегии распараллеливания.
Стоит отметить, что в свежем релизе CUDA 13.3 теперь можно писать высокопроизводительные GPU-ядра на C++, используя CUDA Tile — модель программирования, которая берет на себя управление параллелизмом, перемещением данных и работой с аппаратными функциями NVIDIA. Вместо ручного управления каждым потоком разработчик оперирует многомерными массивами и их тайлами, а компилятор автоматически распараллеливает операции для всех ядер GPU.
Упрощение GPU-программирования
Современная тенденция — уходить в доменно-специфические языки. Чаще всего базой для таких языков выступает Python, один из примеров — Triton. Он позволяет достаточно удобно и просто описывать операции тайлинга, индексации и линейной алгебры.

Можно сказать, что Triton — это такой аналог CUTLASS для Python. Про этот язык мы еще тоже поговорим. В широко распространенных сценариях C++ присутствует в случае, если мы вручную пишем высокооптимизированный код, однако от этого специалисты сейчас стараются уйти.
Тут стоит оговориться, что мой рассказ опирает на реализацию экосистемы PyTorch. Это одна из самых распространенных экосистем, и я хорошо с ней знаком. JAX — это реализация платформы машинного обучения от Google, тоже доменно-специфический язык на основе Python. У него внутри используется просто другой движок и тензорные компиляторы. По сути, это следующее поколение после TensorFlow.
Автоматизация генерации GPU-ядер

Помимо повсеместного использования универсальных AI-ассистентов для написания кода, формируется устойчивый тренд на создание специализированных языковых моделей (LLM). Такие модели дообучаются на специфических датасетах, заточенных под генерацию высокопроизводительных вычислительных ядер.
В качестве примеров можно привести две актуальные разработки, демонстрирующие разные подходы к этой задаче:
CUDA-агент от ByteDance. Система ориентирована на задачи линейной алгебры (например, матричные и векторно-матричные произведения). Пользователь задает математическую формулу или описание операции, а агент генерирует высокооптимизированный исходный код на диалекте C++ (CUDA).
Решение KernelEvo от института AIRI. Этот подход работает на ином, более высоком уровне абстракции. Задача формулируется с помощью набора специальных правил, ограничений и параметров. Одно из ключевых отличий заключается в том, что модель генерирует не исходный C++/CUDA-код, а напрямую выдает промежуточный ассемблер PTX (Parallel Thread Execution). Это позволяет полностью исключить этап компиляции C++ и сразу получать оптимизированный низкоуровневый код, готовый к выполнению на GPU.
Фундамент вычислений: от BLAS до фреймворков
Следующий уровень абстракции — библиотеки, реализующие интерфейс BLAS. Чаще всего это проприетарные, высокооптимизированные под конкретное железо решения от вендоров: Intel oneMKL, NVIDIA cuBLAS, AMD rocBLAS. Существуют и кроссплатформенные open source-реализации, например OpenBLAS.
Поверх BLAS строятся C++-библиотеки с активным использованием шаблонов и метапрограммирования, позволяющие работать в терминах тензоров и строить вычислительные графы. Яркие примеры: Eigen (векторно-матричные операции, оптимизация под CPU), Armadillo (поддерживает бэкенды cuBLAS и CPU), xtensor (современный синтаксис, близкий к NumPy). Фреймворки общего назначения (PyTorch, TensorFlow) под капотом имеют собственные низкоуровневые библиотеки для математики. Например, в PyTorch это библиотека ATen, доступная пользователям через C++-интерфейс LibTorch.

Что нового в С++26 можно использовать для машинного обучения
Многомерные представления (std::mdspan)
Появившийся в C++23 тип позволяет работать с одномерным плоским массивом как с многомерным тензором. Он поддерживает описание порядка доступа к памяти (Row-major, Column-major), срезы и работает без накладных расходов для памяти. Многомерные представления работают как наборы индексов и указателей на определенные области памяти с учетом смещений. Синтаксически это выглядит так:
#include <mdspan> std::vector<int> data; // Представляем плоский массив как матрицу 2x3 std::mdspan ms(data.data(), 2, 3);
Линейная алгебра (std::linalg)
В стандартную библиотеку интегрируется интерфейс BLAS — векторно-матричные операции различных уровней. Стандарт предусматривает, что реализация std::linalg может вызывать вендорно-зависимые библиотеки (cuBLAS, oneMKL) под капотом. Это приводит к множеству вопросов о совместимости и производительности при переносе программы на разные платформы. Синтаксически это выглядит следующим образом:
#include <vector> #include <mdspan> // C++23 #include <linalg> // C++26 std::vector<double> matrix_data = {1.0, 2.0, 3.0, 4.0}; std::vector<double> vector_data = {0.5, 2.0}; std::vector<double> result_data(2, 0.0); // Матрица 2x2 auto A = std::mdspan(matrix_data.data(), 2, 2); // Вектор-столбец auto x = std::mdspan(vector_data.data(), 2); // Вектор для результата auto y = std::mdspan(result_data.data(), 2); std::linalg::matrix_vector_product(A, x, y); double dot_product = std::linalg::dot(x, y);
Это удобно, потому что мы получаем стандартизированный интерфейс. Еще было бы неплохо самостоятельно выбирать бэкенд, но этой опции в стандарте нет.
Параллелизм: многоядерность микропроцессоров
Важная функциональность как самого языка C++, так и его стандартной библиотеки — использование многоядерности микропроцессоров. Начальная реализация появилась еще в C++ 11 как модель памяти и набор классов для работы с потоками, примитивными синхронизациями и атомиками. На высоком уровне у нас есть параллельные алгоритмы, которые на практике выглядят так:
#include <algorithm> #include <execution> std::sort(vec.begin(), vec.end()); std::sort(std::execution::par, vec.begin(), vec.end()); std::sort(std::execution::par_unseq, vec.begin(), vec.end());
Мы можем указать в политике выполнения алгоритма, хотим ли, чтобы при вычислении использовалось несколько ядер или даже par_unseq. Компилятор будет решать, нужно ли просто параллелить или дополнительно использовать SIMD-инструкции.
Сторонние библиотеки для параллельных алгоритмов
Кроме стандартных алгоритмов есть много сторонних библиотек. Одна из самых распространенных — это OneTBB, наследник Intel Threading Building Blocks, которая предоставляет алгоритмы в стиле STL для конструирования параллельных алгоритмов:
oneTBB #include <vector> #include <numeric> #include <tbb/parallel_reduce.h> #include <tbb/blocked_range.h> float parallel_sum(const std::vector<float>& vec) { return tbb::parallel_reduce( tbb::blocked_range<size_t>(0, vec.size()), 0.0f, [&](const tbb::blocked_range<size_t>& r, float running_total) { return std::accumulate(vec.begin() + r.begin(), vec.begin() + r.end(), running_total); }, std::plus<float>()); }
В отличие от стандартной библиотеки, эта предоставляет возможность более тонкой настройки. Можно настроить балансировку нагрузки между потоками, работать с разбиением данных по фрагментами, которые пойдут на разные потоки.
Фреймворк С++ Execution
В C++26 внедрили Execution control library — фреймворк, который вводит абстракции более высокого уровня. В описании параллельных алгоритмов вычислений вводятся понятия «senders», «receivers» и «executors». Senders — это объекты, которые инициируют работу. Receivers получают результаты. Executors — конкретные вычислительные ресурсы. Эти понятия вводятся в основном как интерфейсы и базовые структуры, описания поведения и работы. А реализация зависит от авторов компилятора и реализации стандартной библиотеки.
SYCL
В парадигме STD Execution развивается стандарт от Cronus Group — SYCL, специализированный набор инструментов и компилятор. С SYCL можно написать C++-код, который компилируется для разного целевого оборудования. Код с использованием SYCL выглядит как стандартный код на C++:
#include <sycl/sycl.hpp> std::vector<float> input(N); std::vector<float> output(N); // (GPU > FPGA > CPU) based on the system configuration. sycl::queue q(sycl::default_selector_v); auto exec = sycl_executor(q); // Hypothetical adapter std::transform(std::execution::par.on(exec), input.begin(), input.end(), output.begin(), [](float x) { return x * x + 1.0f; });
Отмечу, что в примере это стандартный C++. Единственное, что здесь добавляется — это SYCL-HPP с дополнительными классами, в которых определяются Executors. И в стандартных алгоритмах уже можно указать, где конкретно хотим выполнять данный код. В зависимости от доступных платформ код будет скомпилирован или с использованием SIMD-операции, или в вычислительном GPU-ядре. Мы не используем диалекты типа OpenCL или CUDA для создания и запуска вычислений. В данный момент это направление в основном развивает компания Intel.
Inference-движки
На уровне inference-движков C++ представлен максимально широко. Такие платформы часто используются на Edge-устройствах, встраиваемых и IoT-решениях, где наличие виртуальной машины Python нежелательно. Поскольку используются уже обученные модели, задача сводится к их эффективному запуску. Поэтому чаще всего ядра движков пишутся на C++ для максимальной производительности.
Классические примеры:
TensorRT (NVIDIA): написан на C++, активно используется на серверах для запуска LLM и на Edge-устройствах (Jetson, Thor).
TensorFlow Lite Micro: C++-движок для bare metal и IoT-устройств.
ExecuTorch (PyTorch): Inference-движок от PyTorch, ориентированный как на серверы, так и на встраиваемые устройства.
Эволюция платформ машинного обучения и изменение роли C++
От статических графов к императивному программированию
Исторически ранние версии фреймворков, таких как TensorFlow, использовали статическое (декларативное) представление вычислительного графа. Граф предварительно строился, компилировался и затем эффективно выполнялся.
PyTorch изменил эту парадигму, внедрив императивный режим. В нем модель описывается как обычная программа с поддержкой условных операторов, динамических размерностей тензоров и циклов, при этом сохраняет автодифференцирование для обучения. Это существенно расширило возможности для R&D и экспериментов, сделав разработку более интуитивной. Позже подобный режим интегрировали и в другие фреймворки, включая TensorFlow.
Ограничения классического императивного подхода
Однако такой подход по сравнению со статическим графом требует компромисса в производительности. При наличии полного вычислительного графа система может провести глобальную оптимизацию, повышая эффективность вычислений на порядок. В классическом императивном режиме каждый вызов операции транслируется из Python в C++ как прямой вызов вычислительного ядра на CPU или GPU. Из-за этого система не видит общую структуру графа и не может выполнить сквозные оптимизации.
Переход к частично и полностью компилируемым графам
Современный подход заключается в переходе к частично или полностью компилируемым графам. Эта парадигма активно развивается в экосистемах PyTorch через механизм torch.compile / Dynamo и JAX.

Специализированный анализатор перехватывает Python-код и строит вычислительный граф, прогоняя его на фиктивных данных без реальных вычислений. В местах, где граф остается целостным, он преобразуется в промежуточное представление и передается тензорному компилятору. Там, где происходят разрывы графа (например, из-за условных операторов или динамических размерностей), система выполняет fallback к стандартному императивному выполнению. Тензорный компилятор генерирует либо оптимизированный код на C++ для CPU, либо код на DSL Triton для GPU, который затем компилируется в исполняемый код для целевой платформы.
Роль C++ в современной инфраструктуре PyTorch
Хотя верхнеуровневые компоненты анализа и работы с графами, например PyTorch Dynamo и части TorchInductor, написаны преимущественно на Python, C++ сохраняет важную роль в реализации конкретных бэкендов тензорного компилятора и в базовой инфраструктуре:
Dispatcher — компонент, который маршрутизирует вызовы вычислительных функций на соответствующие аппаратные бэкенды (CPU, GPU, NPU).
ATen — базовая математическая библиотека на C++. К ней обращаемся, когда граф не поддается существенной оптимизации или нужно выполнить высокоуровневую стандартную операцию.
Анализ и совместная оптимизация графов в PyTorch
Процесс построения графа начинается с анализа Python-байткода и формирования FX-графа. Современная тенденция заключается в совместном анализе прямого и обратного проходов. Это позволяет проводить глубокую оптимизацию использования памяти.
В современных GPU и TPU пропускная способность памяти — это узкое место по сравнению со скоростью вычислений. Компилятору выгоднее перевычислить определенные данные, чем сохранять и перемещать их между уровнями памяти или устройствами. После анализа граф унифицируется и преобразуется в функциональное промежуточное представление, состоящее из чистых функций без побочных эффектов, аналогично тому, как это делается в JAX изначально.
Torch Inductor: режимы компиляции
Далее в работу вступает Torch Inductor — тензорный компилятор с поддержкой различных бэкендов. На этом этапе выполняются оптимизации графа: слияние операций, планирование доступа к памяти, перестановка циклов и оптимизация индексации.
Компилятор поддерживает два основных режима работы:
Just-In-Time (JIT) Inductor: при первом запуске анализирует граф, компилирует его блоки в разделяемые библиотеки (.so-файлы) и загружает их в память для последующего использования.
Ahead-Of-Time (AOT) Inductor: компилирует весь граф в самодостаточный исполняемый бинарный файл. Это позволяет запускать модель на конечном устройстве (включая embedded-системы) даже без полноценной операционной системы.
Генерация кода для CPU и GPU
Для CPU Torch Inductor генерирует реальный C++-код, который выступает в роли высокоуровневого ассемблера. Для стандартных операций (например, умножение матриц или свертка) компилятор может выполнить fallback на вызов внешних оптимизированных библиотек. Для специфичных операций генерируется нативный C++-код, который затем оптимизируется компилятором с использованием автовекторизации (SIMD), и прагм OpenMP для параллeлизации и трансформации циклов.
Для GPU генерируется код на доменно-специфичном языке Triton. Это упрощает разработку, так как вместо низкоуровневой работы с регистрами разработчик или компилятор описывает операции на уровне тензоров: стратегии тайлинга, планирование памяти и индексацию.
Инфраструктура LLVM и MLIR
Triton интегрируется с инфраструктурой LLVM, а именно — с фреймворком Multi-Level Intermediate Representation (MLIR). MLIR позволяет построить конвейер компиляции, который последовательно «понижает» абстракции: от высокоуровневых операций линейной алгебры на Python до специфичных инструкций работы с памятью, циклами и, наконец, до машинного кода конкретного устройства.

Благодаря MLIR один и тот же высокоуровневый код может быть скомпилирован как в нативный ассемблер CUDA, так и в открытый формат шейдеров SPIR-V (используемый, например, в Vulkan), что значительно расширяет кроссплатформенность решений.
Новая роль C++
Где же нашлось место для C++? Инфраструктура LLVM/MLIR, которая обеспечивает всю эту «магию компиляции», полностью написана на C++. Это основной язык реализации компиляторов, API для работы с промежуточными представлениями и создания собственных расширений IR.
В сфере высокопроизводительных вычислений и машинного обучения роль C++ фундаментально трансформируется. Мы переходим от эпохи ручной оптимизации низкоуровневых ядер к использованию C++ в качестве основы для инфраструктуры генерации кода. Кроме того, C++ все чаще выступает в роли «высокоуровневого ассемблера»: тензорные компиляторы генерируют оптимизированный C++-код, который затем компилируется в машинные инструкции целевой платформы.
Хотите узнать больше о С++ в машинном обучении? Вот список полезных источников: