# ROCm MTP Context Sizing — Consolidated Bug Report

## Overview

Native MTP (`--spec-type draft-mtp`) on ROCm reserved disproportionately large compute workspace, reducing available context by ~50% initially, then ~21% after a first round of fixes. Three targeted patches restored context to near-baseline levels.

---

## Problem Statement

| Model | Quantization | Layers | Hardware |
|-------|-------------|--------|----------|
| Qwen3.6-27B | i1-Q4_XS / i1-Q6_K | 65 | ROCm0 (RX 6800, 16 GB) + ROCm1 (RX 6700 XT, 12 GB) |

**Symptom:** MTP draft context caused context to shrink far more than the actual memory it consumed at runtime.

---

## Evidence Timeline

### Phase 1: Initial Discovery (Q4_XS Model)

**With MTP (before any fix):**
```
MTP draft (n_ctx=262,144, 1 layer):
  KV cache:   288 MiB
  Compute:    1,108 MiB  (62 graph nodes, flash attention F16 K/V copies)
  → Maximum context: ~19.4K tokens
```

**Without MTP (baseline):**
```
Main context (n_ctx=19,456, 65 layers):
  KV cache:   456 MiB
  RS buffer:  449 MiB
  Compute:    101 MiB
  → Maximum context: ~37K tokens
```

### Phase 2: Q6 Regression (After Fix 1 + Fix 2)

Old build context: **139,776** tokens  
New build context: **110,080** tokens (lost ~29,696 / ~21%)

Same actual VRAM allocation, but fit algorithm received inflated estimates:

| Scenario | ROCm0 context | ROCm1 context | ROCm0 compute | ROCm1 compute |
|----------|---------------|---------------|---------------|---------------|
| Target only | 166 MiB | 99 MiB | 134 MiB | 164 MiB |
| Target + MTP | 446 MiB | 267 MiB | 134 MiB | 164 MiB |
| **MTP addition** | **+280 MiB** | **+168 MiB** | **+0 MiB** | **+0 MiB** |

### Phase 3: Final Verification (All 3 Fixes)

```
preliminary fit: n_ctx = 139,776
MTP estimate: 153.56 MiB (ctx only, compute skipped)
Final fit: n_ctx = 135,936
  → Within 0.2% of old build baseline (135,680) ✓
```

---

## Root Cause Analysis

### Why ROCm (not Vulkan)

**CUDA/HIP `get_alloc_size`** includes F16 K/V flash-attention workspace copies proportional to context length:

```
K (F16): n_ctx × head_dim × n_kv_heads × 2 bytes
V (F16): n_ctx × head_dim × n_kv_heads × 2 bytes
```

**Vulkan** returns `ggml_nbytes(tensor)` — flash attention temp buffers are pre-allocated on-demand during execution, not counted in measurement.

### Three Distinct Issues

#### Issue 1: MTP draft created with full `n_ctx`

`common/speculative.cpp` used the original requested `n_ctx` instead of the fit-reduced value. The draft context was oversized relative to what the target context actually got.

#### Issue 2: Pre-fit MTP estimate used full `n_ctx`

`tools/server/server-context.cpp` estimated MTP memory with `n_ctx=262,144`, adding ~1,396 MiB to `fit_params_target`. This forced the fit algorithm to shrink context aggressively, even though the actual MTP context would use the smaller fit-reduced `n_ctx`.

#### Issue 3: MTP estimate counted compute buffer

The memory measurement sums `context + compute` for a **standalone** MTP context. But at runtime, MTP draft **shares** the target's compute buffer — it allocates zero additional compute memory. The estimate counted ~1,076 MiB of compute that was never actually allocated.

---

## Fixes

### Fix 1: `common/speculative.cpp` — Runtime MTP draft uses target's fit-reduced n_ctx

```cpp
if (spec_mtp) {
    cparams.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
    cparams.n_ctx = llama_n_ctx(ctx_tgt);  // fit-reduced n_ctx
}
```

Ensures MTP draft context matches the target's (possibly fit-reduced) context size at runtime.

### Fix 2: `tools/server/server-context.cpp` — Pre-fit estimate uses fit-reduced n_ctx

```cpp
if (spec_mtp) {
    cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
    {
        auto mparams_tgt = common_model_params_to_llama(params_base);
        auto cparams_tgt = common_context_params_to_llama(params_base);
        common_fit_params(params_dft.model.path.c_str(), &mparams_tgt, &cparams_tgt,
            params_base.tensor_split,
            params_base.tensor_buft_overrides.data(),
            params_base.fit_params_target.data(),
            params_base.fit_params_min_ctx,
            GGML_LOG_LEVEL_ERROR);
        cparams_dft.n_ctx = cparams_tgt.n_ctx;
    }
}
```

Does a preliminary target-only fit to determine reduced `n_ctx`, then uses that for the MTP estimate. (Note: calls `common_fit_params()` twice; a two-pass design would be cleaner upstream.)

### Fix 3: `tools/server/server-context.cpp` — Skip compute buffer for MTP estimate

```cpp
const size_t bytes = (measure_model_bytes ? dmd[j].model : 0)
    + dmd[j].context
    + (spec_mtp ? 0 : dmd[j].compute);  // MTP shares compute, draft models don't
```

Only counts context memory for MTP since the compute buffer is shared with the target.

---

## Execution Flow (After All Fixes)

```
1. common_base_params_to_speculative(params_base) → params_dft
2. Fix 2: preliminary fit (target-only, n_rs_seq=3, margins=50 MiB/device)
   → cparams_tgt.n_ctx = 139,776
   → cparams_dft.n_ctx = 139,776
3. common_get_device_memory_data with cparams_dft (ctx_type=MTP, n_ctx=139,776)
   → MTP context = 153 MiB, MTP compute = 1,076 MiB (standalone measurement)
4. Fix 3: params_base.fit_params_target[i] += context only (skip compute for MTP)
   → ROCm1: 50 + 153 = 203 MiB (was 50 + 1,229 = 1,280 MiB)
5. common_init_from_params(params_base) → final fit with correct margins
   → cparams.n_ctx = 135,936
6. Fix 1: MTP draft created with n_ctx = llama_n_ctx(ctx_tgt) = 135,936
```

---

## Files Involved

| File | Role |
|------|------|
| `common/speculative.cpp:2275-2279` | Fix 1 — MTP draft uses fit-reduced n_ctx at runtime |
| `tools/server/server-context.cpp:1092-1106` | Fix 2 — pre-fit MTP estimate at fit-reduced n_ctx |
| `tools/server/server-context.cpp:1137-1141` | Fix 3 — skip compute buffer for MTP estimate |
| `common/fit.cpp:29-150` | `common_get_device_memory_data_impl()` — memory measurement |
| `common/fit.cpp:175-358` | `common_params_fit_impl()` — linear interpolation fit |
| `common/common.cpp:1588` | Context params conversion (n_ctx) |
| `ggml/src/ggml-cuda/ggml-cuda.cu:906` | CUDA/HIP `get_alloc_size` (FA workspace) |
| `ggml/src/ggml-cuda/fattn-common.cuh:53` | F16 workspace calculation |
| `ggml/src/ggml-cuda/fattn.cu:536` | Alloc size function |
| `ggml/src/ggml-vulkan/ggml-vulkan.cpp:15642` | Vulkan `get_alloc_size` (no FA workspace) |

---

## Patch Files

| File | Contents |
|------|----------|
| `fix.patch` | Original two-fix patch (superseded) |
| `fix_all.patch` | Combined three-fix patch (current, applies cleanly to main) |

---

## Deployment Notes

`llama-server` is a 17KB thin wrapper. The actual code lives in shared libraries:

- `libllama-server-impl.so` (~7.5 MB) — server logic including the patch
- `libllama-common.so.0.0.832` (~6 MB) — common/fit/speculative code

**Both `.so` libraries must be updated** when deploying a new build. Updating only `llama-server` has no effect.

---

## Conclusion

The `fix_all.patch` (3 fixes, 19 lines total) applies cleanly to `llama.cpp` main branch and restores MTP context sizing to near-baseline levels across both Q4_XS and Q6_K quantizations on multi-GPU ROCm setups.

**Related:** Issue #23903 (MTP draft path buffer allocation after backend-sampling changes).
