NVIDIA CMP 50HX: шанс ещё остаётся…
С чего всё началось
У меня дома стоит самый обычный «подстольный» сервер на 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 |
|
|
whisper.cpp |
|
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 подставьте нужную архитектуру:
|
Карта |
Чип |
|
|---|---|---|
|
CMP 30HX, 40HX, 50HX |
Turing |
|
Проверка проводилась только на 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 |
|
Код для Pascal: без тензорных ядер, MMQ на dp4a |
|
C |
61-virtual |
B + |
B + отключение FMA во всех ядрах |
|
D |
61-virtual |
C + |
C + эмуляция dp4a |
|
E |
75 |
|
Гибрид: тензорные ядра для 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 минут.
Как нейросетевой бум изменил рынок компьютерных комплектующих в 2026 году: что стало с памятью, видеокартами и ценами
SpaceX строит Starlink Mobile следующего поколения без «мёртвых зон»: компания получила дополнительные частоты для связи со смартфонами
«Это вам не маленький аппарат для посадки на Луну»: в отличие от Apollo, Starship позволит строить города на Луне и Марсе
Samsung запустила по всему миру обновление One UI 9 для Galaxy A56 и Galaxy A36
Запускаем у себя аналоги ChatGPT и Claude: пять открытых нейросетей для видеокарт на 8–32 ГБ
iPhone Duo поборолся за лидерство с Samsung Galaxy Z Fold8, Huawei Pura X Max и Xiaomi 18 Fold
Россиянам могут отказать в оформлении загранпаспорта из-за фотографий, обработанных нейросетью
2K, 185 Гц, 10 100 мА·ч, 200 Мп и IP69K: показан Honor Win 2 Delta Force