NVIDIA CMP 50HX: шанс ещё остаётся…

Прокомментировать Просмотры: 5

С чего всё началось

У меня дома стоит самый обычный «подстольный» сервер на Proxmox, где крутится несколько виртуальных машин. Однажды на аукционе я за копейки взял NVIDIA CMP 50HX, чтобы поэкспериментировать с небольшими моделями. Модели запускаю через llama.cpp, а для распознавания речи использую Whisper. Скорость работы меня не радовала, но я быстро с ней свыкся и спокойно пользовался связкой несколько месяцев. Потом мне попалась статья NVIDIA CMP – микроскопы для забивания гвоздей? Копаем глубже….

Настоятельно советую прочитать эту статью. Она сильно проясняет и саму проблему, и путь к её решению, о которых пойдёт речь дальше.

Обращение к «высшему разуму»

Я отправил ссылку на статью «в чат» и попросил предложить решение. Через 20 минут у меня уже был готовый тестовый стенд и патч для исходников ggml. Библиотека ggml общая для llama.cpp и whisper.cpp, поэтому один патч подходит сразу для обоих проектов.

Если сам процесс тестирования вам не интересен, переходите сразу к результатам и готовому решению.

llamacpp-cmp.patch
diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh
index fea1cf9..046728a 100644
--- a/ggml/src/ggml-cuda/common.cuh
+++ b/ggml/src/ggml-cuda/common.cuh
@@ -738,7 +738,17 @@ static __device__ __forceinline__ int ggml_cuda_dp4a(const int a, const int b, i
 
 #else // defined(GGML_USE_HIP)
 
-#if __CUDA_ARCH__ >= GGML_CUDA_CC_DP4A || defined(GGML_USE_MUSA)
+#if defined(GGML_CUDA_CMP_EMU_DP4A) && !defined(GGML_USE_MUSA)
+    // NVIDIA CMP mining GPUs: the hardware dp4a is throttled (about 1/8 rate on CMP 40HX/50HX).
+    // Four plain integer multiply-adds written as inline PTX are faster and are not folded back
+    // into dp4a by the compiler (a plain C expression would be).
+    int r = c;
+    asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a      )), "r"((int)(int8_t)(b      )));
+    asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a >>  8)), "r"((int)(int8_t)(b >>  8)));
+    asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a >> 16)), "r"((int)(int8_t)(b >> 16)));
+    asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a >> 24)), "r"((int)(int8_t)(b >> 24)));
+    return r;
+#elif __CUDA_ARCH__ >= GGML_CUDA_CC_DP4A || defined(GGML_USE_MUSA)
     return __dp4a(a, b, c);
 #else // __CUDA_ARCH__ >= GGML_CUDA_CC_DP4A || defined(GGML_USE_MUSA)
     const int8_t * a8 = (const int8_t *) &a;
diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu
index 9fe618f..c9ed96e 100644
--- a/ggml/src/ggml-cuda/ggml-cuda.cu
+++ b/ggml/src/ggml-cuda/ggml-cuda.cu
@@ -1403,6 +1403,36 @@ static __global__ void k_compute_batched_ptrs(
     ptrs_dst[0*ne23 + i12 + i13*ne12] = (      char *)         dst + i12*nbd2 + i13*nbd3;
 }
 
+#if !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA)
+// NVIDIA CMP mining GPUs (CMP 40HX/50HX/90HX): tensor cores and FMA are throttled, so cuBLAS must be
+// told not to use them (see https://habr.com/ru/articles/948396/: FP16 GEMM goes from ~3 to ~24
+// "units" with CUBLAS_COMPUTE_16F_PEDANTIC on a CMP 50HX).
+// GGML_CUDA_CMP_MODE=1 forces this on, =0 forces it off, unset/other = auto-detect by device name.
+static bool ggml_cuda_device_is_cmp(const int device) {
+    static int8_t cache[GGML_CUDA_MAX_DEVICES] = {0}; // 0 = unknown, 1 = no, 2 = yes
+    if (device < 0 || device >= GGML_CUDA_MAX_DEVICES) {
+        return false;
+    }
+    if (cache[device] == 0) {
+        const char * env = getenv("GGML_CUDA_CMP_MODE");
+        bool is_cmp;
+        if (env != nullptr && env[0] == '0') {
+            is_cmp = false;
+        } else if (env != nullptr && env[0] == '1') {
+            is_cmp = true;
+        } else {
+            cudaDeviceProp prop;
+            is_cmp = cudaGetDeviceProperties(&prop, device) == cudaSuccess && strstr(prop.name, "CMP") != nullptr;
+        }
+        cache[device] = is_cmp ? 2 : 1;
+        if (is_cmp) {
+            GGML_LOG_INFO("%s: device %d treated as NVIDIA CMP: cuBLAS FP16 GEMM without tensor cores (PEDANTIC)\n", __func__, device);
+        }
+    }
+    return cache[device] == 2;
+}
+#endif // !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA)
+
 // Type traits for mapping ggml types to CUDA/cuBLAS types
 template
 struct batched_mul_mat_traits;
@@ -1577,6 +1607,15 @@ static void ggml_cuda_mul_mat_cublas_impl(ggml_backend_cuda_context & ctx, const
     GGML_ASSERT(ne12 % ne02 == 0);
     GGML_ASSERT(ne13 % ne03 == 0);
 
+    auto gemm_algo = CUBLAS_GEMM_DEFAULT_TENSOR_OP;
+#if !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA)
+    if (compute_type == GGML_TYPE_F16 && cu_compute_type == CUBLAS_COMPUTE_16F && ggml_cuda_device_is_cmp(ctx.device)) {
+        // NVIDIA CMP: the tensor-core path is throttled; pure FP16 CUDA-core GEMM is ~7x faster.
+        cu_compute_type = CUBLAS_COMPUTE_16F_PEDANTIC;
+        gemm_algo       = CUBLAS_GEMM_DEFAULT;
+    }
+#endif // !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA)
+
     // broadcast factors
     const int64_t r2 = ne12/ne02;
     const int64_t r3 = ne13/ne03;
@@ -1599,7 +1638,7 @@ static void ggml_cuda_mul_mat_cublas_impl(ggml_backend_cuda_context & ctx, const
                            src1_ptr, cu_data_type_b, s11,
                     beta,   dst_ptr, cu_data_type,   ldc,
                     cu_compute_type,
-                    CUBLAS_GEMM_DEFAULT_TENSOR_OP));
+                    gemm_algo));
     } else if (r2 == 1 && r3 == 1 && is_src0_cont_2 && is_src1_cont_2) {
         // with a [0, 2, 1, 3] perm. and ne02==1 the matrix strides need to be determined from dim 3:
         const int64_t sma = ne02 == 1 ? s03 : s02;
@@ -1615,7 +1654,7 @@ static void ggml_cuda_mul_mat_cublas_impl(ggml_backend_cuda_context & ctx, const
                 beta,   dst_ptr, cu_data_type,   ldc, ne1*ldc, // strideC
                 ne12*ne13,
                 cu_compute_type,
-                CUBLAS_GEMM_DEFAULT_TENSOR_OP));
+                gemm_algo));
     } else {
         // use cublasGemmBatchedEx
         const int64_t ne23 = ne12*ne13;
@@ -1653,7 +1692,7 @@ static void ggml_cuda_mul_mat_cublas_impl(ggml_backend_cuda_context & ctx, const
                 beta,  (      void **) (ptrs_dst.get() + 0*ne23), cu_data_type,   ldc,
                 ne23,
                 cu_compute_type,
-                CUBLAS_GEMM_DEFAULT_TENSOR_OP));
+                gemm_algo));
     }
 
     // Convert output back to F32 if needed

Важное замечание: патч подготовлен под актуальные на момент публикации статьи ветки репозиториев. В llama.cpp (коммит a0e6dc7) он применяется чисто через git apply. В whisper.cpp (коммит 0c7be69) строки смещены на 45, поэтому git apply отказывается его принимать, зато patch -p1 справляется со всеми пятью фрагментами (fuzz 1–2). После наложения патча все три вызова GEMM внутри ggml_cuda_mul_mat_cublas_impl используют новую переменную gemm_algo.

Для более поздних версий патч, скорее всего, придётся перепроверить или переделать.

Что делает патч

1. cuBLAS FP16 без тензорных ядер (ggml-cuda.cu)

В ggml_cuda_mul_mat_cublas_impl добавлена функция ggml_cuda_device_is_cmp(). Она распознаёт карту CMP по названию устройства и запоминает результат. Для таких карт вызовы cublasGemmEx / cublasGemmStridedBatchedEx / cublasGemmBatchedEx получают другие параметры:

  • CUBLAS_COMPUTE_16F → CUBLAS_COMPUTE_16F_PEDANTIC;

  • CUBLAS_GEMM_DEFAULT_TENSOR_OP → CUBLAS_GEMM_DEFAULT.

Режимом управляет переменная окружения GGML_CUDA_CMP_MODE: значение 1 включает его принудительно, 0 отключает, а если переменная не задана, карта определяется автоматически. Когда режим срабатывает, в лог попадает такая строка:

ggml_cuda_device_is_cmp: device 0 treated as NVIDIA CMP: cuBLAS FP16 GEMM without tensor cores (PEDANTIC)

Изменение затрагивает матричное умножение с весами в F16, а это в первую очередь Whisper.

2. Эмуляция dp4a (common.cuh)

Если собрать проект с флагом -DGGML_CUDA_CMP_EMU_DP4A, функция ggml_cuda_dp4a вместо __dp4a выполнит четыре инструкции mad.lo.s32, записанные через Inline-PTX:

int r = c;
asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a      )), "r"((int)(int8_t)(b      )));
asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a >>  8)), "r"((int)(int8_t)(b >>  8)));
asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a >> 16)), "r"((int)(int8_t)(b >> 16)));
asm("mad.lo.s32 %0, %1, %2, %0;" : "+r"(r) : "r"((int)(int8_t)(a >> 24)), "r"((int)(int8_t)(b >> 24)));
return r;

Inline-PTX используется потому, что обычное выражение на C компилятор снова свернул бы в dp4a. На dp4a опираются ядра квантованного умножения матрицы на вектор (MMVQ) и MMQ без тензорных ядер, поэтому эффект прежде всего заметен при генерации токенов.

Подготовка стенда и сборка бенчмарков

Если сам процесс тестирования вам не интересен, переходите сразу к результатам и готовому решению.

0. Мой стенд

GPU

NVIDIA CMP 50HX, 10 ГБ, compute capability 7.5

Драйвер

580.126.09

CUDA Toolkit

12.8 (для сборки), драйвер поддерживает CUDA 13.0

CPU / RAM

12 потоков, 32 ГБ

ОС

Proxmox VE (ядро 6.8.12-pve)

llama.cpp

a0e6dc7 (master на 15.10.2026)

whisper.cpp

0c7be69 (master на 15.10.2026)

1. Проверка оборудования и драйвера

nvidia-smi --query-gpu=name,compute_cap,driver_version,memory.total --format=csv

В колонке name должно присутствовать слово CMP (например, NVIDIA CMP 50HX), иначе автоопределение в патче не сработает. Драйвер должен поддерживать CUDA 12.x: в выводе nvidia-smi строка CUDA Version должна показывать 12.x или выше.

Для других карт CMP вместо 75 подставьте нужную архитектуру:

Карта

Чип

CMAKE_CUDA_ARCHITECTURES

CMP 30HX, 40HX, 50HX

Turing

75

Проверка проводилась только на CMP 50HX

2. Пакеты и CUDA Toolkit

Debian / Ubuntu:

sudo apt update
sudo apt install -y build-essential cmake git ccache curl python3
cmake --version    # нужен 3.18+, проверено на 3.25

Убедитесь, что в выбранном тулките есть nvcc и cuBLAS:

ls $CUDA_ROOT/bin/nvcc
ls $CUDA_ROOT/include/cublas_v2.h $CUDA_ROOT/lib64/libcublas.so*
$CUDA_ROOT/bin/nvcc --version

Если файла cublas_v2.h нет, значит, тулкит установлен не полностью, и лучше взять другой (на моём тестовом сервере это версия 12.8). Установить cuBLAS можно так:

sudo apt install -y cuda-toolkit-12-8     # или libcublas-dev-12-8 при наличии репозитория NVIDIA

Узнайте, на что указывает симлинк /usr/local/cuda:

readlink -f /usr/local/cuda

Если он ведёт не на $CUDA_ROOT (например, на CUDA 13), ничего страшного, но в cmake обязательно передайте -DCUDAToolkit_ROOT. Без этого может возникнуть ошибка линковки cudaGetDeviceProperties_v2.

3. Сборка бенчмарков для тестирования

Чтобы проверить все варианты, я собрал пять конфигураций с разными комбинациями опций и прогнал на них тесты. Каждая конфигурация собиралась в отдельном каталоге с такими общими параметрами:

-DGGML_CUDA=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc

В таблице приведены дополнительные параметры для каждого варианта сборки:

Вариант

Архитектура

Доп. параметры

Сборка

A

75

—

Базовая сборка (без патча)

B

61-virtual

GGML_CUDA_FORCE_MMQ=ON

Код для Pascal: без тензорных ядер, MMQ на dp4a

C

61-virtual

B + -fmad=false

B + отключение FMA во всех ядрах

D

61-virtual

C + -DGGML_CUDA_CMP_EMU_DP4A

C + эмуляция dp4a

E

75

-fmad=false -DGGML_CUDA_CMP_EMU_DP4A

Гибрид: тензорные ядра для MMQ сохранены, FMA и dp4a исключены

Команды для сборки
# A: Базовая сборка (без патча)
cmake -B build_a -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc -DCMAKE_CUDA_ARCHITECTURES=75
# B: Код Pascal: без тензорных ядер, MMQ на dp4a
cmake -B build_b -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc -DCMAKE_CUDA_ARCHITECTURES="61-virtual" -DGGML_CUDA_FORCE_MMQ=ON
# C: B + запрет FMA во всех ядрах
cmake -B build_c -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc -DCMAKE_CUDA_ARCHITECTURES="61-virtual" -DGGML_CUDA_FORCE_MMQ=ON \
      -DCMAKE_CUDA_FLAGS="-fmad=false"
# D: C + эмуляция dp4a
cmake -B build_d -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc -DCMAKE_CUDA_ARCHITECTURES="61-virtual" -DGGML_CUDA_FORCE_MMQ=ON \
      -DCMAKE_CUDA_FLAGS="-fmad=false -DGGML_CUDA_CMP_EMU_DP4A"
# E: Гибрид: тензорные ядра для MMQ сохранены, FMA и dp4a убраны
cmake -B build_e -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc -DCMAKE_CUDA_ARCHITECTURES=75 -fmad=false -DGGML_CUDA_CMP_EMU_DP4A

for n in a b c d e; do cmake --build build_$n -j --target llama-bench; done

Результаты: llama.cpp

Методика замеров

  • LLM: llama-bench -m <модель> -p 512 -n 128 -ngl 99, по 5 повторов (значение по умолчанию). Метрика pp512 показывает скорость обработки промпта из 512 токенов (prefill), а tg128 — скорость генерации 128 токенов. Обе величины измеряются в токенах в секунду: чем больше, тем лучше.

  • Флаги запуска: -fa 1 -ctk q8_0 -ctv q8_0 (flash attention и KV-кэш в q8_0).

  • Whisper: whisper-cli с моделью large-v3-turbo на файле samples/jfk.wav (11 секунд аудио); фиксировались время кодирования (encode) и общее время.

  • Чистота эксперимента: перед каждым замером видеопамять GPU была свободна (0 МиБ), а один прогон занимал около 5 минут.

1. Qwen3.5-9B Q4_K_M (5,28 ГиБ)

Источник

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов