Skip to content

Add H100 Hopper throughput autotune and server plan integration - #34

Open
Jackson57279 wants to merge 2 commits into
masterfrom
feat/h100-hopper-throughput-autotune
Open

Add H100 Hopper throughput autotune and server plan integration#34
Jackson57279 wants to merge 2 commits into
masterfrom
feat/h100-hopper-throughput-autotune

Conversation

@Jackson57279

@Jackson57279 Jackson57279 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds H100 as a GPU cluster family (CLI, core profiles, K8s manifest generation) alongside B200/A100/RTX Pro 6000.
  • Extends autotune with tier-9 Hopper throughput rules: for a single H100 with full GPU offload, selects paged KV, FlashAttention-3-class attention, CUDA graphs, persistent decode kernels, chunked prefill, and quantization-aware weight/KV plans (W4A16/W8A8/FP8).
  • Wires autotune into oxidize-server and the CLI API server path: load_model_runtime_with_plan applies plan values to server args (batch mode, KV dtype, TurboQuant, prefill chunk size) and configures the PagedAttention scheduler/block pool from the plan.
  • Mirrors the new autotune fields and H100 test coverage in oxidize-cpp.

Test plan

  • cargo test -p oxidize-core h100_31b
  • cargo test -p oxidize-server effective_batch_mode
  • cargo test -p oxidize-server server_autotune
  • cargo test -p oxidize-cpp (autotune_test)
  • Manual: oxidize-server --model <q4_k_m.gguf> --auto --print-plan on H100 hardware

Made with Cursor


Summary by cubic

Adds H100 Hopper as a GPU family and a throughput autotune plan; the server now applies this plan to auto-enable paged KV, Q4 TurboQuant, FlashAttention-3, CUDA graphs, and plan-based scheduler/block-pool sizing on H100 when --auto. No breaking changes; override any setting with explicit flags.

  • New Features

    • Autotune (tier-9 Hopper) in oxidize-core: adds WeightPlan/AttentionKernel, chunked prefill, max decode batch, TPS estimates, and tests.
    • Server integration in oxidize-server: new load_model_runtime_with_plan and effective_batch_mode; applies plan to args (threads, ctx size, batch mode, KV dtype, TurboQuant, prefill chunk); paged runtime sizes BlockPool KV dtype and Scheduler (prefill chunk, max decode batch) from the plan; adds --prefill-chunk-size (default 16).
    • CLI/Cluster: gpu-cluster generate supports --family h100 with a default H100 node pool; API server path uses the plan to build a paged runtime; plan JSON now includes kv_quantization, weight/attention/CUDA fields.
    • C++ mirror (oxidize-cpp): exposes new plan fields, detects H100 via nvidia-smi, updates JSON/summary output, adds an H100 throughput test.
  • Dependencies

    • Bumped anyhow to 1.0.103 to address RUSTSEC-2026-0190.

Written for commit 36ee327. Summary will update on new commits.

Review in cubic

…plans.

Introduce GpuFamily::H100 cluster profiles, tier-9 autotune rules for single-GPU Hopper throughput (paged KV, FlashAttention-3, CUDA graphs), and apply those plans when loading the server so batch mode, KV dtype, and scheduler limits follow autotune output.
@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="oxidize-server/src/main.rs">

<violation number="1" location="oxidize-server/src/main.rs:45">
P3: Startup logs can report the wrong batch mode when autotune overrides `--batch-mode`, which makes production debugging and rollout validation misleading. Consider logging the computed `batch_mode` value (or emitting both requested/effective modes) so telemetry matches the runtime path.</violation>
</file>

<file name="oxidize-server/src/runtime/paged.rs">

<violation number="1" location="oxidize-server/src/runtime/paged.rs:131">
P2: Requests can stall indefinitely when prefill chunk size is set to 0, because the scheduler never makes prefill progress with a zero chunk. Clamping `prefill_chunk_size` to at least 1 when building `SchedulerConfig` avoids this no-progress state.</violation>
</file>

<file name="oxidize-core/src/autotune/rules.rs">

<violation number="1" location="oxidize-core/src/autotune/rules.rs:559">
P2: H100 throughput overrides are applied even for partial GPU offload, because the new guard only checks `n_gpu_layers > 0` instead of full offload. That can force paged/FA3/cuda-graphs/decode-batch settings on mixed CPU+GPU runs where the profile assumptions do not hold; consider gating this tier to `n_gpu_layers == model.layer_count`.</violation>

<violation number="2" location="oxidize-core/src/autotune/rules.rs:601">
P2: The conditional `if matches!(plan.weight_plan, WeightPlan::W4A16) && model.layer_count >= 48` block at the end of `tier9_hopper_throughput` always sets `plan.max_decode_batch = 16`, but that value was already set unconditionally earlier in the same function. The conditional is a no-op — it does not alter `max_decode_batch` regardless of weight plan or layer count.

The presence of the guard (`W4A16 && layer_count >= 48`) suggests it was meant to apply a *different* batch size for large W4A16 models (likely a higher value like 32) but the literal `16` was reused inadvertently. As written, the entire `if` block can be removed without changing behavior. If a larger batch size for large W4A16 models was intended, the conditional value should be corrected.</violation>
</file>

<file name="oxidize-core/src/cluster/gpu_cluster.rs">

<violation number="1" location="oxidize-core/src/cluster/gpu_cluster.rs:134">
P2: H100 nodes will be labeled as not MIG-capable even though Hopper supports MIG, which can skew scheduling/capability decisions that rely on `nvidia.com/mig.capable`. The H100 profile would be safer with `mig_capable: true` (and policy-specific MIG enablement kept separate).</violation>
</file>

<file name="oxidize-cpp/include/oxidize/autotune.hpp">

<violation number="1" location="oxidize-cpp/include/oxidize/autotune.hpp:23">
P2: `ModelFingerprint` fields `layer_count`, `num_kv_heads`, and `head_dim` are declared but never populated or consumed. `fingerprint_model_file()` only sets `file_size_bytes`, and `plan_cpu()` never references the new fields. Readers of the struct (and the test that laboriously sets them) will reasonably assume these values influence the tuning plan, but they don't. Consider either (a) populating them from GGUF metadata in `fingerprint_model_file()` and using them in H100/KV-sizing logic, or (b) removing them until a consumer exists.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


SchedulerConfig {
max_num_batched_tokens: args.prefill_batch_size,
prefill_chunk_size: planned_prefill_chunk.min(args.prefill_batch_size.max(1)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Requests can stall indefinitely when prefill chunk size is set to 0, because the scheduler never makes prefill progress with a zero chunk. Clamping prefill_chunk_size to at least 1 when building SchedulerConfig avoids this no-progress state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At oxidize-server/src/runtime/paged.rs, line 131:

<comment>Requests can stall indefinitely when prefill chunk size is set to 0, because the scheduler never makes prefill progress with a zero chunk. Clamping `prefill_chunk_size` to at least 1 when building `SchedulerConfig` avoids this no-progress state.</comment>

<file context>
@@ -99,3 +88,121 @@ pub fn build_paged_runtime(args: &Args, runtime: Arc<ModelRuntime>) -> Arc<Paged
+
+    SchedulerConfig {
+        max_num_batched_tokens: args.prefill_batch_size,
+        prefill_chunk_size: planned_prefill_chunk.min(args.prefill_batch_size.max(1)),
+        max_num_running_seqs,
+    }
</file context>
Suggested change
prefill_chunk_size: planned_prefill_chunk.min(args.prefill_batch_size.max(1)),
prefill_chunk_size: planned_prefill_chunk.max(1).min(args.prefill_batch_size.max(1)),

model: &ModelFingerprint,
plan: &mut TuningPlan,
) {
if inv.gpu_family != Some(crate::gpu_cluster::GpuFamily::H100) || plan.n_gpu_layers == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: H100 throughput overrides are applied even for partial GPU offload, because the new guard only checks n_gpu_layers > 0 instead of full offload. That can force paged/FA3/cuda-graphs/decode-batch settings on mixed CPU+GPU runs where the profile assumptions do not hold; consider gating this tier to n_gpu_layers == model.layer_count.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At oxidize-core/src/autotune/rules.rs, line 559:

<comment>H100 throughput overrides are applied even for partial GPU offload, because the new guard only checks `n_gpu_layers > 0` instead of full offload. That can force paged/FA3/cuda-graphs/decode-batch settings on mixed CPU+GPU runs where the profile assumptions do not hold; consider gating this tier to `n_gpu_layers == model.layer_count`.</comment>

<file context>
@@ -501,6 +551,67 @@ fn tier8_pipeline(inv: &HardwareInventory, model: &ModelFingerprint, plan: &mut
+    model: &ModelFingerprint,
+    plan: &mut TuningPlan,
+) {
+    if inv.gpu_family != Some(crate::gpu_cluster::GpuFamily::H100) || plan.n_gpu_layers == 0 {
+        return;
+    }
</file context>

memory_mib: 81_920,
tdp_watts: 700,
nvlink: true,
mig_capable: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: H100 nodes will be labeled as not MIG-capable even though Hopper supports MIG, which can skew scheduling/capability decisions that rely on nvidia.com/mig.capable. The H100 profile would be safer with mig_capable: true (and policy-specific MIG enablement kept separate).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At oxidize-core/src/cluster/gpu_cluster.rs, line 134:

<comment>H100 nodes will be labeled as not MIG-capable even though Hopper supports MIG, which can skew scheduling/capability decisions that rely on `nvidia.com/mig.capable`. The H100 profile would be safer with `mig_capable: true` (and policy-specific MIG enablement kept separate).</comment>

<file context>
@@ -114,6 +124,18 @@ pub fn profile(family: GpuFamily) -> GpuProfile {
+            memory_mib: 81_920,
+            tdp_watts: 700,
+            nvlink: true,
+            mig_capable: false,
+            time_slice_replicas: 1,
+            network_class: "infiniband",
</file context>


struct ModelFingerprint {
uint64_t file_size_bytes = 0;
size_t layer_count = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ModelFingerprint fields layer_count, num_kv_heads, and head_dim are declared but never populated or consumed. fingerprint_model_file() only sets file_size_bytes, and plan_cpu() never references the new fields. Readers of the struct (and the test that laboriously sets them) will reasonably assume these values influence the tuning plan, but they don't. Consider either (a) populating them from GGUF metadata in fingerprint_model_file() and using them in H100/KV-sizing logic, or (b) removing them until a consumer exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At oxidize-cpp/include/oxidize/autotune.hpp, line 23:

<comment>`ModelFingerprint` fields `layer_count`, `num_kv_heads`, and `head_dim` are declared but never populated or consumed. `fingerprint_model_file()` only sets `file_size_bytes`, and `plan_cpu()` never references the new fields. Readers of the struct (and the test that laboriously sets them) will reasonably assume these values influence the tuning plan, but they don't. Consider either (a) populating them from GGUF metadata in `fingerprint_model_file()` and using them in H100/KV-sizing logic, or (b) removing them until a consumer exists.</comment>

<file context>
@@ -12,16 +13,35 @@ struct HardwareInventory {
 
 struct ModelFingerprint {
   uint64_t file_size_bytes = 0;
+  size_t layer_count = 0;
+  size_t num_kv_heads = 0;
+  size_t head_dim = 0;
</file context>

}
}

if matches!(plan.weight_plan, WeightPlan::W4A16) && model.layer_count >= 48 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The conditional if matches!(plan.weight_plan, WeightPlan::W4A16) && model.layer_count >= 48 block at the end of tier9_hopper_throughput always sets plan.max_decode_batch = 16, but that value was already set unconditionally earlier in the same function. The conditional is a no-op — it does not alter max_decode_batch regardless of weight plan or layer count.

The presence of the guard (W4A16 && layer_count >= 48) suggests it was meant to apply a different batch size for large W4A16 models (likely a higher value like 32) but the literal 16 was reused inadvertently. As written, the entire if block can be removed without changing behavior. If a larger batch size for large W4A16 models was intended, the conditional value should be corrected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At oxidize-core/src/autotune/rules.rs, line 601:

<comment>The conditional `if matches!(plan.weight_plan, WeightPlan::W4A16) && model.layer_count >= 48` block at the end of `tier9_hopper_throughput` always sets `plan.max_decode_batch = 16`, but that value was already set unconditionally earlier in the same function. The conditional is a no-op — it does not alter `max_decode_batch` regardless of weight plan or layer count.

The presence of the guard (`W4A16 && layer_count >= 48`) suggests it was meant to apply a *different* batch size for large W4A16 models (likely a higher value like 32) but the literal `16` was reused inadvertently. As written, the entire `if` block can be removed without changing behavior. If a larger batch size for large W4A16 models was intended, the conditional value should be corrected.</comment>

<file context>
@@ -501,6 +551,67 @@ fn tier8_pipeline(inv: &HardwareInventory, model: &ModelFingerprint, plan: &mut
+        }
+    }
+
+    if matches!(plan.weight_plan, WeightPlan::W4A16) && model.layer_count >= 48 {
+        plan.max_decode_batch = 16;
+    }
</file context>

let (model_opt, paged_opt) = if args.batch_mode == BatchMode::Paged {
if let Some(runtime) = model {
let paged = build_paged_runtime(&args, runtime.clone());
let batch_mode = effective_batch_mode(&args, loaded_model.autotune_plan.as_ref());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Startup logs can report the wrong batch mode when autotune overrides --batch-mode, which makes production debugging and rollout validation misleading. Consider logging the computed batch_mode value (or emitting both requested/effective modes) so telemetry matches the runtime path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At oxidize-server/src/main.rs, line 45:

<comment>Startup logs can report the wrong batch mode when autotune overrides `--batch-mode`, which makes production debugging and rollout validation misleading. Consider logging the computed `batch_mode` value (or emitting both requested/effective modes) so telemetry matches the runtime path.</comment>

<file context>
@@ -33,24 +33,26 @@ async fn main() {
-    let (model_opt, paged_opt) = if args.batch_mode == BatchMode::Paged {
-        if let Some(runtime) = model {
-            let paged = build_paged_runtime(&args, runtime.clone());
+    let batch_mode = effective_batch_mode(&args, loaded_model.autotune_plan.as_ref());
+    let (model_opt, paged_opt) = if batch_mode == BatchMode::Paged {
+        if let Some(runtime) = loaded_model.runtime {
</file context>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@Jackson57279

Copy link
Copy Markdown
Contributor Author

Triage note (maintainer pass): green CI, self-contained, and the server wiring (load_model_runtime_with_plan applying the plan to batch mode / KV dtype / TurboQuant / prefill chunk) is the kind of end-to-end integration the KV-cache PR is missing — good.

Before merge, the test plan still has two unchecked boxes:

  • cargo test -p oxidize-cpp (autotune_test) for the C++ mirror of the new plan fields — the Rust side is covered but the C++ mirror isn't confirmed here.
  • Manual --auto --print-plan on real H100 hardware.

The first is cheap and should be green already given the matrix passes — please tick it or say if it's blocked. The H100 manual check can be a post-merge follow-up since the tier-9 rules are guarded behind --auto and overridable. Also carries the anyhow 1.0.103 bump that several other PRs also have — dedupe on rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant