Skip to content

Commit f8d4133

Browse files
fix: correct MinInferenceVRAMGB and TotalVRAMGB calculation in PD planner (#1236)
Co-authored-by: cemeng <cemengzhang@gmail.com>
1 parent 40ab4e5 commit f8d4133

4 files changed

Lines changed: 105 additions & 43 deletions

File tree

common/types/pd_planner.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,15 @@ const DefaultGPUsPerNode = 8
346346
// - precision: inference precision (fp8, bf16, etc.)
347347
// - hiddenSize: hidden size from config.json (used to estimate NonMoEParamsB)
348348
// - numHiddenLayers: number of hidden layers from config.json
349+
// - minInferenceVRAMGB: minimum VRAM per GPU to load and run inference, typically
350+
// from metadata.mini_gpu_memory_gb. When <= 0, falls back to DefaultGPUUnitGB (80).
349351
//
350352
// The function:
351353
// 1. Estimates NonMoEParamsB from hidden_size and num_hidden_layers when available,
352354
// otherwise falls back to the active/total expert ratio heuristic.
353355
// 2. Plans prefill and decode configurations using PlanPD.
354356
// 3. Returns a PDRecommendation suitable for storage as JSONB.
355-
func PlanPDRecommendation(modelName string, totalParamsB float64, totalExperts, activeExperts int, precision string, hiddenSize, numHiddenLayers int) (*PDRecommendation, error) {
357+
func PlanPDRecommendation(modelName string, totalParamsB float64, totalExperts, activeExperts int, precision string, hiddenSize, numHiddenLayers int, minInferenceVRAMGB float64) (*PDRecommendation, error) {
356358
if totalParamsB <= 0 {
357359
return nil, fmt.Errorf("total params must be positive, got %f", totalParamsB)
358360
}
@@ -396,23 +398,30 @@ func PlanPDRecommendation(modelName string, totalParamsB float64, totalExperts,
396398
return nil, fmt.Errorf("failed to plan PD for model %s: %w", modelName, err)
397399
}
398400

401+
// When minInferenceVRAMGB is 0 (metadata.mini_gpu_memory_gb not populated),
402+
// keep it as 0 so the missing value is visible rather than masked by a
403+
// default. PD planning checks still pass; only the VRAM fields are
404+
// inaccurate until the value is resolved from TOML or metadata.
405+
if minInferenceVRAMGB < 0 {
406+
minInferenceVRAMGB = 0
407+
}
408+
399409
return &PDRecommendation{
400410
ModelName: modelName,
401411
TotalParamsB: spec.TotalParamsB,
402412
NonMoEParamsB: spec.NonMoEParamsB,
403413
TotalExperts: spec.TotalExperts,
404414
ActiveExperts: activeExperts,
405415
Precision: spec.Precision,
406-
MinInferenceVRAMGB: DefaultGPUUnitGB,
407-
Prefill: planResultToRoleConfig(prefill),
408-
Decode: planResultToRoleConfig(decode),
416+
MinInferenceVRAMGB: minInferenceVRAMGB,
417+
Prefill: planResultToRoleConfig(prefill, minInferenceVRAMGB),
418+
Decode: planResultToRoleConfig(decode, minInferenceVRAMGB),
409419
}, nil
410420
}
411421

412422
// planResultToRoleConfig converts a PDPlanResult into a PDRoleConfig.
413-
// TotalVRAMGB is computed as MinInferenceVRAMGB * TotalGPUs by the caller
414-
// (PlanPDRecommendation sets MinInferenceVRAMGB = DefaultGPUUnitGB).
415-
func planResultToRoleConfig(result PDPlanResult) PDRoleConfig {
423+
// TotalVRAMGB is computed as minInferenceVRAMGB * Pods.
424+
func planResultToRoleConfig(result PDPlanResult, minInferenceVRAMGB float64) PDRoleConfig {
416425
pods := result.LWSSize
417426
if pods < 1 {
418427
pods = 1
@@ -423,6 +432,6 @@ func planResultToRoleConfig(result PDPlanResult) PDRoleConfig {
423432
DP: result.DP,
424433
TotalGPUs: result.TotalGPUs,
425434
Pods: pods,
426-
TotalVRAMGB: float64(result.TotalGPUs) * DefaultGPUUnitGB,
435+
TotalVRAMGB: minInferenceVRAMGB * float64(pods),
427436
}
428437
}

common/types/pd_planner_test.go

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -256,27 +256,56 @@ func TestPlanPD_FullMatrix(t *testing.T) {
256256
}
257257

258258
func TestPlanPDRecommendation_DeepSeekV3(t *testing.T) {
259-
rec, err := PlanPDRecommendation("deepseek-v3", 671, 256, 8, "fp8", 7168, 61)
259+
// minInferenceVRAMGB=772.0 matches configs/pd/models.toml for DeepSeek-V3.
260+
rec, err := PlanPDRecommendation("deepseek-v3", 671, 256, 8, "fp8", 7168, 61, 772.0)
260261
require.NoError(t, err)
261262
require.NotNil(t, rec)
262263
require.Equal(t, 671.0, rec.TotalParamsB)
263264
require.Equal(t, 256, rec.TotalExperts)
264265
require.Equal(t, 8, rec.ActiveExperts)
265266
require.Equal(t, "fp8", rec.Precision)
266-
require.Equal(t, 80.0, rec.MinInferenceVRAMGB)
267+
require.Equal(t, 772.0, rec.MinInferenceVRAMGB)
267268
require.Greater(t, rec.Prefill.TotalGPUs, 0)
268269
require.Greater(t, rec.Decode.TotalGPUs, 0)
269-
require.Greater(t, rec.Prefill.TotalVRAMGB, 0.0)
270+
// TotalVRAMGB = MinInferenceVRAMGB * Pods
271+
require.Equal(t, 772.0*float64(rec.Prefill.Pods), rec.Prefill.TotalVRAMGB)
272+
require.Equal(t, 772.0*float64(rec.Decode.Pods), rec.Decode.TotalVRAMGB)
270273
require.Greater(t, rec.Prefill.Pods, 0)
271274
require.Greater(t, rec.Decode.Pods, 0)
272275

273-
t.Logf("DeepSeek-V3 80G: Prefill TP=%d EP=%d GPUs=%d Pods=%d | Decode TP=%d EP=%d GPUs=%d Pods=%d",
274-
rec.Prefill.TP, rec.Prefill.EP, rec.Prefill.TotalGPUs, rec.Prefill.Pods,
275-
rec.Decode.TP, rec.Decode.EP, rec.Decode.TotalGPUs, rec.Decode.Pods)
276+
t.Logf("DeepSeek-V3 80G: Prefill TP=%d EP=%d GPUs=%d Pods=%d VRAM=%.0f | Decode TP=%d EP=%d GPUs=%d Pods=%d VRAM=%.0f",
277+
rec.Prefill.TP, rec.Prefill.EP, rec.Prefill.TotalGPUs, rec.Prefill.Pods, rec.Prefill.TotalVRAMGB,
278+
rec.Decode.TP, rec.Decode.EP, rec.Decode.TotalGPUs, rec.Decode.Pods, rec.Decode.TotalVRAMGB)
279+
}
280+
281+
func TestPlanPDRecommendation_MinInferenceVRAMZero(t *testing.T) {
282+
// When minInferenceVRAMGB is 0 (metadata.mini_gpu_memory_gb not populated),
283+
// keep it as 0 so the missing value is visible. PD planning still succeeds;
284+
// only the VRAM fields are 0 until the value is resolved from TOML/metadata.
285+
rec, err := PlanPDRecommendation("deepseek-v3", 671, 256, 8, "fp8", 7168, 61, 0)
286+
require.NoError(t, err)
287+
require.NotNil(t, rec)
288+
require.Equal(t, 0.0, rec.MinInferenceVRAMGB)
289+
// TotalVRAMGB = 0 * Pods = 0
290+
require.Equal(t, 0.0, rec.Prefill.TotalVRAMGB)
291+
require.Equal(t, 0.0, rec.Decode.TotalVRAMGB)
292+
// PD planning checks should still pass
293+
require.Greater(t, rec.Prefill.TotalGPUs, 0)
294+
require.Greater(t, rec.Decode.TotalGPUs, 0)
295+
}
296+
297+
func TestPlanPDRecommendation_MinInferenceVRAMNegative(t *testing.T) {
298+
// Negative values should be clamped to 0.
299+
rec, err := PlanPDRecommendation("deepseek-v3", 671, 256, 8, "fp8", 7168, 61, -10)
300+
require.NoError(t, err)
301+
require.NotNil(t, rec)
302+
require.Equal(t, 0.0, rec.MinInferenceVRAMGB)
303+
require.Equal(t, 0.0, rec.Prefill.TotalVRAMGB)
304+
require.Equal(t, 0.0, rec.Decode.TotalVRAMGB)
276305
}
277306

278307
func TestPlanPDRecommendation_GLM52(t *testing.T) {
279-
rec, err := PlanPDRecommendation("glm-5.2", 1000, 256, 8, "fp8", 8192, 80)
308+
rec, err := PlanPDRecommendation("glm-5.2", 1000, 256, 8, "fp8", 8192, 80, 0)
280309
require.NoError(t, err)
281310
require.NotNil(t, rec)
282311
require.Equal(t, 1000.0, rec.TotalParamsB)
@@ -285,7 +314,7 @@ func TestPlanPDRecommendation_GLM52(t *testing.T) {
285314
}
286315

287316
func TestPlanPDRecommendation_Qwen3_30B_A3B(t *testing.T) {
288-
rec, err := PlanPDRecommendation("qwen3-30b-a3b", 30, 128, 8, "bf16", 2048, 48)
317+
rec, err := PlanPDRecommendation("qwen3-30b-a3b", 30, 128, 8, "bf16", 2048, 48, 0)
289318
require.NoError(t, err)
290319
require.NotNil(t, rec)
291320
require.Equal(t, 30.0, rec.TotalParamsB)
@@ -294,7 +323,7 @@ func TestPlanPDRecommendation_Qwen3_30B_A3B(t *testing.T) {
294323

295324
func TestPlanPDRecommendation_UnknownModelMoE(t *testing.T) {
296325
// Unknown MoE model with expert info from config.json
297-
rec, err := PlanPDRecommendation("custom/moe-model", 200, 64, 8, "bf16", 4096, 32)
326+
rec, err := PlanPDRecommendation("custom/moe-model", 200, 64, 8, "bf16", 4096, 32, 0)
298327
require.NoError(t, err)
299328
require.NotNil(t, rec)
300329
require.Equal(t, 200.0, rec.TotalParamsB)
@@ -306,7 +335,7 @@ func TestPlanPDRecommendation_UnknownModelMoE(t *testing.T) {
306335
}
307336

308337
func TestPlanPDRecommendation_DenseModel(t *testing.T) {
309-
rec, err := PlanPDRecommendation("custom-dense-70b", 70, 0, 0, "bf16", 0, 0)
338+
rec, err := PlanPDRecommendation("custom-dense-70b", 70, 0, 0, "bf16", 0, 0, 0)
310339
require.NoError(t, err)
311340
require.NotNil(t, rec)
312341
require.Equal(t, 70.0, rec.TotalParamsB)
@@ -315,35 +344,35 @@ func TestPlanPDRecommendation_DenseModel(t *testing.T) {
315344
}
316345

317346
func TestPlanPDRecommendation_ZeroParams(t *testing.T) {
318-
_, err := PlanPDRecommendation("test", 0, 256, 8, "fp8", 0, 0)
347+
_, err := PlanPDRecommendation("test", 0, 256, 8, "fp8", 0, 0, 0)
319348
require.Error(t, err)
320349
require.Contains(t, err.Error(), "total params must be positive")
321350
}
322351

323352
func TestPlanPDRecommendation_NegativeParams(t *testing.T) {
324-
_, err := PlanPDRecommendation("test", -10, 256, 8, "fp8", 0, 0)
353+
_, err := PlanPDRecommendation("test", -10, 256, 8, "fp8", 0, 0, 0)
325354
require.Error(t, err)
326355
require.Contains(t, err.Error(), "total params must be positive")
327356
}
328357

329358
func TestPlanPDRecommendation_PrecisionOverride(t *testing.T) {
330359
// Known model with explicit precision override
331360
// Use a smaller model that fits in 80GB with bf16
332-
rec, err := PlanPDRecommendation("qwen3-30b-a3b", 30, 128, 8, "bf16", 2048, 48)
361+
rec, err := PlanPDRecommendation("qwen3-30b-a3b", 30, 128, 8, "bf16", 2048, 48, 0)
333362
require.NoError(t, err)
334363
require.Equal(t, "bf16", rec.Precision)
335364
}
336365

337366
func TestPlanPDRecommendation_ExpertOverride(t *testing.T) {
338367
// Override expert count from config.json
339-
rec, err := PlanPDRecommendation("deepseek-v3", 671, 512, 16, "fp8", 7168, 61)
368+
rec, err := PlanPDRecommendation("deepseek-v3", 671, 512, 16, "fp8", 7168, 61, 0)
340369
require.NoError(t, err)
341370
require.Equal(t, 512, rec.TotalExperts)
342371
require.Equal(t, 16, rec.ActiveExperts)
343372
}
344373

345374
func TestPlanPDRecommendation_DPField(t *testing.T) {
346-
rec, err := PlanPDRecommendation("deepseek-v3", 671, 256, 8, "fp8", 7168, 61)
375+
rec, err := PlanPDRecommendation("deepseek-v3", 671, 256, 8, "fp8", 7168, 61, 0)
347376
require.NoError(t, err)
348377
// DP should default to 1 for all recommendations
349378
require.Equal(t, 1, rec.Prefill.DP)

common/types/pd_recommendation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ type PDRoleConfig struct {
7575
// PDConfig.PrefillReplicas/DecodeReplicas (default 1), which HPA scales up/down.
7676
Pods int `json:"pods"`
7777
// TotalVRAMGB is the total VRAM required for this role, computed as
78-
// MinInferenceVRAMGB * TotalGPUs. Used for VRAM validation and hardware splitting ratio.
78+
// MinInferenceVRAMGB * Pods. Used for VRAM validation and hardware splitting ratio.
7979
TotalVRAMGB float64 `json:"total_vram_gb"`
8080
}
8181

@@ -97,7 +97,7 @@ type PDRecommendation struct {
9797
// Precision is the inference precision.
9898
Precision string `json:"precision"`
9999
// MinInferenceVRAMGB is the minimum VRAM per GPU required to load and run inference.
100-
// TotalVRAMGB for each role is computed as MinInferenceVRAMGB * TotalGPUs.
100+
// TotalVRAMGB for each role is computed as MinInferenceVRAMGB * Pods.
101101
MinInferenceVRAMGB float64 `json:"min_inference_vram_gb"`
102102
// Prefill is the recommended prefill configuration.
103103
Prefill PDRoleConfig `json:"prefill"`

configs/pd/models.toml

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# pods - number of pods per LWS group (LWS Size)
2121
#
2222
# total_vram_gb is NOT stored in TOML — it is computed in code as:
23-
# total_vram_gb = min_inference_vram_gb * total_gpus
23+
# total_vram_gb = min_inference_vram_gb * pods
2424
#
2525
# ─── TP/EP/DP Convention ───
2626
# models.toml stores TP and EP with the SAME value (n), DP=1.
@@ -36,13 +36,13 @@
3636
# ─── DeepSeek ───
3737

3838
[[models]]
39-
model_name = "deepseek-ai/DeepSeek-V3"
40-
total_params_b = 671.0
41-
non_moe_params_b = 37.0
39+
model_name = "deepseek-ai/DeepSeek-V4-Flash-DSpark"
40+
total_params_b = 284.0
41+
non_moe_params_b = 13.0
4242
total_experts = 256
43-
active_experts = 8
43+
active_experts = 6
4444
precision = "fp8"
45-
min_inference_vram_gb = 772.0
45+
min_inference_vram_gb = 327.0
4646

4747
[models.prefill]
4848
tp = 8
@@ -52,11 +52,11 @@ total_gpus = 8
5252
pods = 1
5353

5454
[models.decode]
55-
tp = 16
56-
ep = 16
55+
tp = 8
56+
ep = 8
5757
dp = 1
58-
total_gpus = 16
59-
pods = 2
58+
total_gpus = 8
59+
pods = 1
6060

6161
[[models]]
6262
model_name = "deepseek-ai/DeepSeek-V4-Flash"
@@ -81,6 +81,30 @@ dp = 1
8181
total_gpus = 8
8282
pods = 1
8383

84+
[[models]]
85+
model_name = "deepseek-ai/DeepSeek-V4-Pro-DSpark"
86+
total_params_b = 1600.0
87+
non_moe_params_b = 49.0
88+
total_experts = 384
89+
active_experts = 6
90+
precision = "fp8"
91+
min_inference_vram_gb = 1840.0
92+
93+
[models.prefill]
94+
tp = 16
95+
ep = 16
96+
dp = 1
97+
total_gpus = 16
98+
pods = 2
99+
100+
[models.decode]
101+
tp = 32
102+
ep = 32
103+
dp = 1
104+
total_gpus = 32
105+
pods = 4
106+
107+
84108
[[models]]
85109
model_name = "deepseek-ai/DeepSeek-V4-Pro"
86110
total_params_b = 1600.0
@@ -233,18 +257,18 @@ precision = "bf16"
233257
min_inference_vram_gb = 33.0
234258

235259
[models.prefill]
236-
tp = 2
237-
ep = 2
260+
tp = 1
261+
ep = 1
238262
dp = 1
239-
total_gpus = 2
240-
pods = 2
263+
total_gpus = 1
264+
pods = 1
241265

242266
[models.decode]
243-
tp = 2
244-
ep = 2
267+
tp = 1
268+
ep = 1
245269
dp = 1
246-
total_gpus = 2
247-
pods = 2
270+
total_gpus = 1
271+
pods = 1
248272

249273
# ─── Kimi (Moonshot) ───
250274

0 commit comments

Comments
 (0)