
1 测试背景
本次测试对比了两个不同架构的深度学习工作站的性能表现。一方面是基于Intel i9-10980XE和NVIDIA RTX 4090的传统x86_64平台,另一方面是基于ARM Cortex-X925和NVIDIA GB10显卡的ThinkStation PGX工作站。测试旨在评估两个平台在典型深度学习工作负载下的实际表现。
2 测试环境配置对比
2.1 硬件配置对比表
| 配置项 | ThinkStation PGX | RTX 4090系统 | 备注 |
|---|---|---|---|
| CPU架构 | ARM aarch64 (Cortex-X925/A725) | x86_64 (Intel i9-10980XE) | 架构差异显著 |
| CPU核心数 | 20核 | 36线程(18核) | 4090系统多线程更强 |
| CPU频率 | 最高4.0GHz | 固定3.0GHz | PGX单核频率更高 |
| GPU型号 | NVIDIA GB10 | NVIDIA GeForce RTX 4090 | 专业卡vs消费卡 |
| GPU架构 | 推测为Ampere架构(警告显示12.1) | Ada Lovelace架构(8.9) | 不同代架构 |
| 显存容量 | 119.6 GB | 24 GB | PGX显存大5倍 |
2.2 软件环境对比表
| 软件组件 | ThinkStation PGX | RTX 4090系统 | 影响分析 |
|---|---|---|---|
| 操作系统 | Ubuntu Linux 6.14.0 | WSL2 Linux 6.6.87 | 均为Linux环境 |
| Python版本 | 3.10.19 | 3.10.19 | 版本一致 |
| PyTorch版本 | 2.9.1+cu130 | 2.9.0+cu126 | 版本相近 |
| CUDA版本 | 13.0 | 12.6 | PGX版本更新 |
3 基准测试性能对比
3.1 详细性能数据表
| 测试项目 | ThinkStation PGX性能 | RTX 4090性能 | 性能倍数 (4090/PGX) | 性能差距分析 |
|---|---|---|---|---|
| 矩阵乘法 (4096×4096, FP16) | 2.413 ms 56,958 GFLOPS | 0.873 ms 157,486 GFLOPS | 2.76倍 | 4090计算吞吐量显著领先 |
| 卷积运算 (Conv2D backward, FP16) | 13.638 ms 814 GFLOPS | 4.089 ms 2,714 GFLOPS | 3.33倍 | 4090卷积优化更佳 |
| 训练步骤 (batch=16, AMP) | 18.03 ms 888 samples/sec | 6.32 ms 2,531 samples/sec | 2.85倍 | 端到端训练速度快2.85倍 |
| 显存容量 | 119.6 GB | 24 GB | 0.20倍 (1/5) | PGX显存容量优势明显 |
| 系统内存 | 128 GB | 64 GB | 0.5倍 | PGX系统内存多一倍 |
3.2 性能对比可视化
性能对比 矩阵乘法 GFLOPS 卷积运算 GFLOPS 训练速度 samples/sec PGX: 56,958 4090: 157,486 PGX: 814 4090: 2,714 PGX: 888 4090: 2,531
4 深度技术分析
4.1 计算性能差异原因
RTX 4090的优势来源:
- 架构先进性:Ada Lovelace架构针对AI计算优化,具有更高的能效比
- 显存带宽:GDDR6X显存提供高达1TB/s的带宽,远超专业卡的大容量显存
- 核心频率:4090的GPU核心频率更高,计算单元响应更快
- 软件优化:消费级显卡有更成熟的驱动和库优化
PGX系统性能限制因素:
- CUDA兼容性问题 :GPU能力12.1超出PyTorch支持范围,可能导致:
- 无法使用最新的Tensor Core功能
- 某些优化路径被禁用
- 潜在的性能损失
- ARM CPU与GPU协同:ARM架构与NVIDIA GPU的协同可能不如x86成熟
- 专业卡优化方向:专业卡可能更注重稳定性而非峰值性能
4.2 内存系统分析
PGX的内存优势:
- 超大显存适合的应用场景 :
- 训练超大模型(如千亿参数模型)
- 处理大规模图像/视频数据
- 需要极大批次的训练任务
- 多模型同时加载推理
- 大系统内存优势 :
- 数据预处理流水线更流畅
- 支持更大的磁盘缓存
- 多任务并行能力更强
4090的内存局限与应对:
- 24GB显存对现代大模型可能不足
- 但可通过以下方式缓解:
- 梯度累积技术
- 模型并行/流水线并行
- 更精细的内存管理
4.3 架构差异影响
x86_64 (Intel) 优势:
- 成熟的软件生态
- 更好的库优化(如Intel MKL)
- 广泛的生产环境部署经验
ARM (Cortex-X925) 优势:
- 潜在的能效比优势
- 现代指令集支持(SVE2等)
- 新兴的AI优化架构
5 实际应用场景建议
5.1 推荐使用RTX 4090的场景
-
学术研究与原型开发
- 模型迭代速度快,需要快速实验
- 多数研究模型可在24GB内运行
- 计算性能优先,快速验证想法
-
中小规模模型训练
- 参数规模在10B以下
- 需要快速收敛和调参
- 对训练时间敏感的项目
-
推理服务部署
- 需要高吞吐量推理
- 多模型快速切换
- 对延迟敏感的应用
-
预算有限但追求性能
- 4090通常有更好的性价比
- 更低的功耗和散热需求
- 更容易获得的硬件
5.2 推荐使用ThinkStation PGX的场景
-
超大模型训练与微调
- 参数规模超过30B的模型
- 需要完整加载到显存的场景
- LoRA/QLoRA微调大模型
-
大规模数据处理
- 高分辨率图像/视频处理
- 科学计算中的大型矩阵运算
- 需要大缓存的数据流水线
-
多任务并行环境
- 同时运行多个模型推理
- 研究环境中的多用户共享
- 生产环境中的冗余备份
-
长期稳定性要求
- 7×24小时不间断运行
- 企业级可靠性和支持
- 长期维护的部署
6 成本效益分析
6.1 直接成本考虑
- RTX 4090系统:通常2,000-3,000(不包括其他组件)
- ThinkStation PGX:专业工作站,估计5,000-10,000+
运营成本
- 电力消耗:4090可能更高(TDP 450W vs 专业卡优化)
- 散热需求:4090需要更强散热解决方案
- 维护成本:PGX可能有更好的企业支持
6.2 投资回报分析
场景1:快速迭代研究
- 4090:更快的结果 = 更快的论文发表 = 更高的学术产出
- PGX:可能因计算慢而延误进展
场景2:生产环境部署
- 4090:可能存在显存瓶颈,需要更多优化
- PGX:一次加载大模型,简化部署流程
7 总结与最终建议
- 性能差距显著:RTX 4090在计算性能上全面领先,平均快2.8-3.3倍
- 内存容量悬殊:PGX拥有近5倍的显存容量,适合内存密集型任务
- 架构选择关键:x86成熟稳定,ARM新兴有潜力
- 软件生态影响:PGX存在兼容性警告,可能影响性能发挥
| 用户类型 | 主要需求 | 推荐平台 | 关键理由 |
|---|---|---|---|
| 研究者/学生 | 快速实验,预算有限 | RTX 4090 | 性能足够,成本可控,生态成熟 |
| 创业公司 | 产品原型,快速迭代 | RTX 4090 | 开发效率高,部署灵活 |
| 企业研发 | 大规模模型,稳定运行 | ThinkStation PGX | 大内存支持,企业级可靠性 |
| 云服务商 | 多租户,高利用率 | 根据需求混合 | 4090用于通用推理,PGX用于大模型 |
| 科研机构 | 前沿探索,超大模型 | ThinkStation PGX | 硬件能力支撑创新研究 |
没有绝对的最佳选择,只有最合适的选择。
-
如果你的工作流程注重速度 、模型尺寸适中 、需要快速迭代,那么RTX 4090是更明智的选择。它能以更低成本提供卓越的计算性能,加速你的研究和开发进程。
-
如果你的工作涉及超大模型 、需要处理海量数据 、注重长期稳定运行,那么ThinkStation PGX的大内存优势将变得至关重要。它为特定专业场景提供了必要的硬件基础。
8 测试程序和脚本
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
benchmark_pytorch.py
通用 PyTorch 基准程序:打印系统/设备信息,运行 matmul/conv/training 基准,支持 AMP。
"""
import os
import sys
import time
import argparse
import platform
import subprocess
import json
from datetime import datetime
import torch
import torch.nn as nn
import torch.nn.functional as F
# ---------- Utility / System info ----------
def run_cmd(cmd):
try:
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True, timeout=10)
return out.strip()
except Exception as e:
return f"(failed: {e})"
def print_system_info():
print("="*80)
print("系统与硬件信息 (captured {})".format(datetime.utcnow().isoformat() + "Z"))
print("- Hostname:", run_cmd("hostname"))
print("- uname -a:", run_cmd("uname -a"))
print("- platform.machine():", platform.machine())
print("- Python:", sys.version.replace("\n"," "))
print("- PyTorch:", torch.__version__)
print("- Torch CUDA available:", torch.cuda.is_available())
if torch.cuda.is_available():
try:
print("- CUDA runtime version:", torch.version.cuda)
except:
pass
try:
# device count
print("- torch.cuda.device_count():", torch.cuda.device_count())
for i in range(torch.cuda.device_count()):
props = torch.cuda.get_device_properties(i)
print(f" - CUDA device {i}: name={props.name}, major={props.major}, minor={props.minor}, total_memory={props.total_memory/1024**3:.2f} GB")
# memory info with torch.cuda.mem_get_info if available
try:
free, total = torch.cuda.mem_get_info(i)
print(f" - mem_get_info: free={free/1024**3:.3f} GB total={total/1024**3:.3f} GB")
except Exception:
pass
except Exception as e:
print(" - Error querying torch.cuda:", e)
# try nvidia-smi
print("- nvidia-smi:")
print(run_cmd("nvidia-smi -L || true"))
print(run_cmd("nvidia-smi --query-gpu=name,index,memory.total,memory.used,utilization.gpu --format=csv,noheader,nounits || true"))
# CPU info
print("- lscpu (first 30 lines):")
print("\n".join(run_cmd("lscpu || true").splitlines()[:30]))
print("- /proc/cpuinfo (first 20 lines):")
print("\n".join(run_cmd("cat /proc/cpuinfo || true").splitlines()[:20]))
# memory
print("- free -h:")
print(run_cmd("free -h || true"))
# top processes (brief)
print("- top -b -n1 | head -n 20")
print(run_cmd("top -b -n1 | head -n 20 || true"))
print("="*80)
# ---------- Timing helpers ----------
def time_cuda(fwd, warmup=10, iters=50):
# fwd: callable that runs one iteration. Assumes CUDA device.
for _ in range(warmup):
fwd()
torch.cuda.synchronize()
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in range(iters):
fwd()
torch.cuda.synchronize()
t1 = time.perf_counter()
avg = (t1 - t0) / iters
return avg
def time_cpu(f, warmup=10, iters=50):
for _ in range(warmup):
f()
t0 = time.perf_counter()
for _ in range(iters):
f()
t1 = time.perf_counter()
return (t1 - t0) / iters
# ---------- Benchmarks ----------
def bench_matmul(device, dtype, M=4096, K=4096, N=4096, iters=50, warmup=10, backward=False):
print(f"[MATMUL] device={device} dtype={dtype} dims={M}x{K} * {K}x{N} backward={backward}")
torch.manual_seed(123)
t_dtype = getattr(torch, dtype)
if device.startswith("cuda") and torch.cuda.is_available():
dev = torch.device(device)
a = torch.randn((M, K), dtype=t_dtype, device=dev, requires_grad=backward)
b = torch.randn((K, N), dtype=t_dtype, device=dev, requires_grad=backward)
def one():
y = torch.matmul(a, b)
if backward:
y.sum().backward()
avg = time_cuda(one, warmup=warmup, iters=iters)
else:
dev = torch.device("cpu")
a = torch.randn((M, K), dtype=t_dtype, device=dev, requires_grad=backward)
b = torch.randn((K, N), dtype=t_dtype, device=dev, requires_grad=backward)
def one():
y = torch.matmul(a, b)
if backward:
y.sum().backward()
avg = time_cpu(one, warmup=warmup, iters=iters)
# FLOPS: 2*M*K*N for matmul (fwd). If backward included, multiply estimate approx x2.
flops_forward = 2.0 * M * K * N
flops_total = flops_forward * (2.0 if backward else 1.0)
t = avg
gflops = (flops_total / t) / 1e9
print(f" avg latency per iter: {t*1000:.3f} ms | estimated GFLOPS: {gflops:.2f}")
return {"latency_s": t, "gflops": gflops}
def bench_conv2d(device, dtype, batch=32, in_c=3, out_c=64, H=224, W=224, kernel=3, iters=50, warmup=10, backward=True):
print(f"[CONV2D] device={device} dtype={dtype} B={batch} C_in={in_c} C_out={out_c} HxW={H}x{W} k={kernel} backward={backward}")
t_dtype = getattr(torch, dtype)
dev = torch.device(device if (device.startswith("cuda") and torch.cuda.is_available()) else "cpu")
x = torch.randn((batch, in_c, H, W), dtype=t_dtype, device=dev, requires_grad=backward)
conv = nn.Conv2d(in_c, out_c, kernel_size=kernel, stride=1, padding=kernel//2).to(dev).to(t_dtype)
opt = torch.optim.SGD(conv.parameters(), lr=0.01)
def one():
opt.zero_grad()
y = conv(x)
if backward:
loss = y.sum()
loss.backward()
opt.step()
if dev.type == "cuda":
avg = time_cuda(one, warmup=warmup, iters=iters)
else:
avg = time_cpu(one, warmup=warmup, iters=iters)
# simple FLOPs estimate for conv forward: 2*B*out_c*in_c*KH*KW*OH*OW (approx)
OH, OW = H, W
flops_forward = 2.0 * batch * out_c * in_c * kernel * kernel * OH * OW
if backward:
flops_total = flops_forward * 2.0 # rough estimate
else:
flops_total = flops_forward
gflops = (flops_total / avg) / 1e9
print(f" avg latency per iter: {avg*1000:.3f} ms | estimated GFLOPS: {gflops:.2f}")
return {"latency_s": avg, "gflops": gflops}
class SimpleModel(nn.Module):
def __init__(self, in_c=3, num_classes=1000):
super().__init__()
self.conv1 = nn.Conv2d(in_c, 32, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(32)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(64, num_classes)
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.conv2(x))
x = self.pool(x).view(x.size(0), -1)
return self.fc(x)
def bench_training_step(device, dtype, batch=32, input_size=(3,224,224), iters=50, warmup=10, amp=False):
"""
修复版:当 amp=True 时 保持模型为 float32,不对模型 .to(dtype);
仅在 amp=False 且 dtype!=float32 时才 cast 模型/输入到指定 dtype。
同时使用 torch.amp namespace(避免 FutureWarning)。
"""
print(f"[TRAIN_STEP] device={device} dtype={dtype} batch={batch} input={input_size} amp={amp}")
t_dtype = getattr(torch, dtype)
dev = torch.device(device if (device.startswith("cuda") and torch.cuda.is_available()) else "cpu")
# 如果使用 AMP(推荐),保持模型为 float32
if amp and dev.type == "cuda":
model = SimpleModel(in_c=input_size[0], num_classes=1000).to(dev).to(torch.float32)
else:
# 否则按照用户指定的 dtype(float32/float16/bfloat16)
model = SimpleModel(in_c=input_size[0], num_classes=1000).to(dev).to(t_dtype)
opt = torch.optim.SGD(model.parameters(), lr=0.01)
# 使用新的 amp API(torch.amp)
scaler = torch.amp.GradScaler() if (amp and dev.type == "cuda") else None
# 输入 tensor:当 amp=True 时保持 float32(让 autocast 决定哪些 ops 用 fp16)
if amp and dev.type == "cuda":
x = torch.randn((batch,)+input_size, dtype=torch.float32, device=dev)
else:
x = torch.randn((batch,)+input_size, dtype=t_dtype, device=dev)
target = torch.randint(0,1000,(batch,), device=dev)
loss_fn = nn.CrossEntropyLoss()
def one():
opt.zero_grad()
if scaler is not None:
# 新 API:torch.amp.autocast('cuda')
with torch.amp.autocast(device_type='cuda'):
out = model(x)
loss = loss_fn(out, target)
scaler.scale(loss).backward()
scaler.step(opt)
scaler.update()
else:
out = model(x)
loss = loss_fn(out, target)
loss.backward()
opt.step()
if dev.type=="cuda":
avg = time_cuda(one, warmup=warmup, iters=iters)
else:
avg = time_cpu(one, warmup=warmup, iters=iters)
samples_per_sec = (batch / avg)
print(f" avg step latency: {avg*1000:.2f} ms | samples/sec: {samples_per_sec:.1f}")
return {"latency_s": avg, "samples_per_sec": samples_per_sec}
# ---------- CLI ----------
def parse_args():
p = argparse.ArgumentParser(description="PyTorch benchmark (matmul/conv/training) + system info")
p.add_argument("--device", default="cuda", help="device: cuda or cpu or cuda:0")
p.add_argument("--dtype", default="float32", choices=["float32","float16","bfloat16"], help="datatype")
p.add_argument("--task", default="all", choices=["matmul","conv","train","all"], help="which benchmark")
p.add_argument("--matmul-size", type=int, nargs=3, metavar=('M','K','N'), default=[4096,4096,4096])
p.add_argument("--conv-batch", type=int, default=32)
p.add_argument("--train-batch", type=int, default=16)
p.add_argument("--iters", type=int, default=50)
p.add_argument("--warmup", type=int, default=10)
p.add_argument("--use-amp", action="store_true", help="use torch.cuda.amp for training (cuda only)")
return p.parse_args()
def main():
args = parse_args()
# print system info first
print_system_info()
# set deterministic/cudnn settings
torch.backends.cudnn.benchmark = True
device = args.device
if device.startswith("cuda") and not torch.cuda.is_available():
print("[WARN] CUDA requested but not available. Falling back to CPU.")
device = "cpu"
results = {}
if args.task in ("matmul","all"):
M,K,N = args.matmul_size
results['matmul'] = bench_matmul(device, args.dtype, M=M, K=K, N=N, iters=args.iters, warmup=args.warmup, backward=False)
if args.task in ("conv","all"):
results['conv'] = bench_conv2d(device, args.dtype, batch=args.conv_batch, in_c=3, out_c=64, H=224, W=224, kernel=3, iters=args.iters, warmup=args.warmup, backward=True)
if args.task in ("train","all"):
results['train'] = bench_training_step(device, args.dtype, batch=args.train_batch, input_size=(3,224,224), iters=args.iters, warmup=args.warmup, amp=args.use_amp)
print("\nSUMMARY (json):")
print(json.dumps(results, indent=2))
print("Done.")
if __name__ == "__main__":
main()
运行脚本
#!/usr/bin/env bash
set -euo pipefail
# run_benchmark.sh
PY=python3
SCRIPT=benchmark_pytorch.py
# default args
DEVICE="cuda"
DTYPE="float32"
TASK="all"
ITERS=50
USE_AMP=0
# parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--device) DEVICE="$2"; shift 2;;
--dtype) DTYPE="$2"; shift 2;;
--task) TASK="$2"; shift 2;;
--iters) ITERS="$2"; shift 2;;
--amp) USE_AMP=1; shift ;;
*) echo "Unknown arg: $1"; exit 1;;
esac
done
echo ">>> Starting benchmark at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
ARCH=$(uname -m)
echo "Detected architecture: $ARCH"
# recommended env tuning (adjust NUM_THREADS to your CPU)
export OMP_NUM_THREADS=${OMP_NUM_THREADS:-4}
export MKL_NUM_THREADS=${MKL_NUM_THREADS:-4}
export OPENBLAS_NUM_THREADS=${OPENBLAS_NUM_THREADS:-4}
echo "OMP_NUM_THREADS=$OMP_NUM_THREADS MKL_NUM_THREADS=$MKL_NUM_THREADS OPENBLAS_NUM_THREADS=$OPENBLAS_NUM_THREADS"
# optional: pin to NUMA or set CUDA_VISIBLE_DEVICES if needed
# export CUDA_VISIBLE_DEVICES=0
CMD="$PY $SCRIPT --device $DEVICE --dtype $DTYPE --task $TASK --iters $ITERS --warmup 10"
if [ "$USE_AMP" -eq 1 ]; then
CMD="$CMD --use-amp"
fi
echo "Running: $CMD"
eval "$CMD"
echo ">>> Done at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
运行命令
bash
./run_benchmark.sh --device cuda --dtype float16 --task all --iters 80 --amp