Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions BONSAI_TERNARY_MIG_PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Bonsai Ternary 8B MIG Deployment — Performance Test Results

## Overview
This document records the deployment and performance testing of the **Bonsai Ternary 1.58-bit 8B** model on four NVIDIA MIG `1g.10gb` instances in the local Kubernetes cluster.

- **Namespace:** `bonsai-ternary`
- **Deployment:** `bonsai-ternary-mig`
- **Replicas:** 4 (one per available MIG instance)
- **Proxy:** LiteLLM (`litellm-bonsai-proxy`) exposed on `http://100.115.213.88:4000/v1`
- **Model file:** `Ternary-Bonsai-8B-Q2_0.gguf` (mounted via hostPath)

## Final Deployment Configuration

### llama-server Command
```yaml
- /opt/prism-release/llama-prism-b8846-d104cf1/llama-server
- --host
- "0.0.0.0"
- --port
- "8080"
- -m
- /model/Ternary-Bonsai-8B-Q2_0.gguf
- --ctx-size
- "65536"
- --parallel
- "2"
- --cache-type-k
- "q8_0"
- --cache-type-v
- "q8_0"
- --threads
- "8"
```

### Resource Requests
```yaml
limits:
nvidia.com/mig-1g.10gb: 1
requests:
nvidia.com/mig-1g.10gb: 1
```

### Host Library Mounts
- `/usr/lib/x86_64-linux-gnu/libcuda.so.1`
- `/usr/lib/x86_64-linux-gnu/libgomp.so.1`
- `/usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.1`

### Proxy
The LiteLLM proxy is configured with `hostNetwork: true` and listens directly on host port `4000`, routing to the backend Kubernetes service `bonsai-ternary-mig:8080`.

## Test Methodology

Tests were run from the host against:
```
http://100.115.213.88:4000/v1/completions
```

Payload template:
```json
{
"model": "bonsai-ternary-8b",
"prompt": "Once upon a time",
"max_tokens": <N>,
"temperature": 0.0
}
```

Metrics collected:
- `tokens_predicted` from the response
- `timings.predicted_per_second` from the response
- Wall-clock time for the full test run
- Aggregate throughput = total tokens / wall-clock time

## Test Results

### 1. Baseline — 4K context, parallel=1, f16 KV
Configuration: `--ctx-size 4096 --parallel 1`

| Metric | Value |
|---|---|
| Requests | 8 |
| Concurrency | 4 |
| max_tokens | 4000 |
| Completed | 8/8 |
| Total tokens | 32,000 |
| Wall-clock time | 573.99s |
| **Aggregate throughput** | **55.75 tok/s** |
| Single-stream throughput | ~28 tok/s |
| Average latency | 197.40s |

### 2. 64K context, parallel=1, f16 KV (CPU offload)
Configuration: `--ctx-size 65536 --parallel 1`

| Metric | Value |
|---|---|
| Context size | 65536 |
| KV cache | 3328 MiB CPU + 5888 MiB GPU |
| 100-token smoke test | **63.66s (~1.6 tok/s)** |
| 6000-token batch test | **Timed out at 600s** |

The KV cache exceeded GPU memory and was partially offloaded to host RAM, causing severe slowdown.

### 3. 64K context, parallel=1, q8_0 KV
Configuration: `--ctx-size 65536 --parallel 1 --cache-type-k q8_0 --cache-type-v q8_0`

| Metric | Value |
|---|---|
| KV cache | 4896 MiB GPU |
| GPU memory per instance | ~7224 MiB |
| Requests | 4 |
| Concurrency | 4 |
| max_tokens | 4000 |
| Completed | 4/4 |
| Total tokens | 16,000 |
| Wall-clock time | 455.52s |
| **Aggregate throughput** | **35.12 tok/s** |
| Single-stream throughput | ~25 tok/s |
| Average latency | 237.61s |

q8_0 KV cache quantization restored usable throughput, but aggregate remained limited because only one request could run per backend at a time.

### 4. 64K context, parallel=2, q8_0 KV (final configuration)
Configuration: `--ctx-size 65536 --parallel 2 --cache-type-k q8_0 --cache-type-v q8_0`

Server confirmation:
```
llama_context: n_seq_max = 2
llama_context: n_ctx_seq = 32768
llama_kv_cache: CUDA0 KV buffer size = 4896.00 MiB
```

| Metric | Value |
|---|---|
| Slots per instance | 2 × 32K |
| KV cache | 4896 MiB GPU |
| GPU memory per instance | ~7224–7232 MiB |
| Requests | 8 |
| Concurrency | 8 |
| max_tokens | 4000 |
| Completed | 8/8 |
| Total tokens | 26,399 |
| Wall-clock time | 295.53s |
| **Aggregate throughput** | **89.33 tok/s** |
| Single-stream throughput | ~23–25 tok/s |
| Average latency | 180.68s |

This is near the theoretical maximum of ~100 tok/s for four MIG instances.

## Final Observations

- **vLLM is incompatible** with the Bonsai Ternary 1.58-bit GGUF format; it fails with `ValueError: np.uint32(42) is not a valid GGMLQuantizationType`. The Prism fork of llama.cpp is required.
- **64K context is viable** on 10GB MIG partitions only with KV-cache quantization (`q8_0`) and parallelism to keep the cache in GPU memory.
- **Aggregate throughput scales with backend concurrency**: moving from `parallel=1` to `parallel=2` increased aggregate throughput from 35 tok/s to 89 tok/s.
- Single-stream speed is slightly lower at 64K context (~23–25 tok/s) compared to 4K context (~28 tok/s) due to larger compute graph overhead.

## Files Created

- `/home/mctouch/code/production-stack/perf_test.py` — fixed-batch throughput test
- `/home/mctouch/code/production-stack/perf_test_continuous.py` — continuous saturation test (not run to completion due to long per-request times)
- `/tmp/bonsai-ternary-mig-deployment.yaml` — current deployment manifest
- `/home/mctouch/code/production-stack/BONSAI_TERNARY_MIG_PERFORMANCE.md` — this file
Comment on lines +158 to +161

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.

medium

The documentation contains absolute local file paths specific to a user's environment (e.g., /home/mctouch/... and /tmp/...). These should be updated to use relative paths relative to the repository root so that they are correct for all users and environments.


## Recommended Configuration

Use the final settings from Test 4 for maximum token throughput on the 10GB MIG partitions:

```yaml
--ctx-size 65536
--parallel 2
--cache-type-k q8_0
--cache-type-v q8_0
--threads 8
```

Access via LiteLLM proxy:
```
http://100.115.213.88:4000/v1
```

---

### 5. Three MIG instances — 64K context, parallel=2, q8_0 KV
After removing one replica (GPU 1), the deployment scaled down to 3 instances on GPUs 2, 3, and 4.

Configuration: `--ctx-size 65536 --parallel 2 --cache-type-k q8_0 --cache-type-v q8_0`

| Metric | Value |
|---|---|
| Active MIG instances | 3 |
| Slots available | 6 (2 per instance) |
| Requests | 6 |
| Concurrency | 6 |
| max_tokens | 4000 |
| Completed | 6/6 |
| Total tokens | 22,133 |
| Wall-clock time | 455.48s |
| **Aggregate throughput** | **48.59 tok/s** |
| Single-stream throughput | ~22–25 tok/s |
| Average latency | 208.44s |

Throughput scaled proportionally with the loss of one backend: from **89.33 tok/s** with 4 instances to **48.59 tok/s** with 3 instances.

## Node Loss Event Log

When the deployment was scaled from 4 to 3 replicas, Kubernetes recorded the following events:

```
32m Normal Pulled pod/bonsai-ternary-mig-55cbd8979b-vzj5h Container image "localhost:5000/llama.cpp-server:prism-b8846-d104cf1" already present on machine
32m Normal Created pod/bonsai-ternary-mig-55cbd8979b-vzj5h Created container llama-server
32m Normal Started pod/bonsai-ternary-mig-55cbd8979b-vzj5h Started container llama-server
30m Normal Killing pod/bonsai-ternary-mig-55cbd8979b-vzj5h Stopping container llama-server
30m Normal SuccessfulDelete replicaset/bonsai-ternary-mig-55cbd8979b Deleted pod: bonsai-ternary-mig-55cbd8979b-vzj5h
30m Normal ScalingReplicaSet deployment/bonsai-ternary-mig Scaled down replica set bonsai-ternary-mig-55cbd8979b to 3 from 4
```

Availability after the scale-down:

```
NAME READY UP-TO-DATE AVAILABLE
bonsai-ternary-mig 3/3 3 3
```

The LiteLLM proxy continued serving requests on the remaining 3 backends without requiring a restart.
51 changes: 51 additions & 0 deletions deployment-bonsai-ternary.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bonsai-ternary-mig
namespace: bonsai-ternary
labels:
app: bonsai-ternary-mig
spec:
replicas: 4
selector:
matchLabels:
app: bonsai-ternary-mig
template:
metadata:
labels:
app: bonsai-ternary-mig
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
terminationGracePeriodSeconds: 30
Comment on lines +18 to +20

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.

high

Using hostNetwork: true on a deployment with replicas: 4 will cause port binding conflicts on port 8080 if multiple replicas are scheduled on the same Kubernetes node (which is the standard case for MIG partitions on a single GPU/node). Since a Kubernetes Service bonsai-ternary-mig is used to route traffic to the pods, hostNetwork: true is unnecessary and should be removed to allow proper scheduling and port isolation.

      terminationGracePeriodSeconds: 30

containers:
- name: llama-server
image: llama.cpp-server:prism-b8846-d104cf1
command:
- "/opt/prism-release/llama-server"
- "--host"
- "0.0.0.0"
- "--port"
- "8080"
- "-m"
- "/model/Ternary-Bonsai-8B-Q2_0.gguf"
- "--ctx-size"
- "2048"
- "--threads"
- "8"
Comment on lines +32 to +35

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.

medium

The deployment manifest is configured with a context size of 2048 and is missing the optimized settings (--parallel 2, --cache-type-k q8_0, --cache-type-v q8_0) recommended in BONSAI_TERNARY_MIG_PERFORMANCE.md for optimal throughput. Let's update the command arguments to match the recommended configuration.

        - "--ctx-size"
        - "65536"
        - "--parallel"
        - "2"
        - "--cache-type-k"
        - "q8_0"
        - "--cache-type-v"
        - "q8_0"
        - "--threads"
        - "8"

resources:
limits:
nvidia.com/mig-1g.10gb: 1
requests:
nvidia.com/mig-1g.10gb: 1
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: model-storage
mountPath: /model
readOnly: true
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: ollama-models-store-pvc
78 changes: 78 additions & 0 deletions perf_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Throughput performance test for the Bonsai Ternary MIG deployment (3 instances, 64K context, q8_0 KV, parallel=2)."""
import requests
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

URL = "http://100.115.213.88:4000/v1/completions"
MODEL = "bonsai-ternary-8b"
PROMPT = "Once upon a time"
MAX_TOKENS = 4000
CONCURRENCY = 6
TOTAL_REQUESTS = 6
TIMEOUT = 600

def make_request(i):
payload = {
"model": MODEL,
"prompt": PROMPT,
"max_tokens": MAX_TOKENS,
"temperature": 0.0,
}
start = time.perf_counter()
try:
resp = requests.post(URL, json=payload, timeout=TIMEOUT)
latency = time.perf_counter() - start
if resp.status_code != 200:
return {"error": f"status {resp.status_code}: {resp.text[:200]}", "latency": latency}
data = resp.json()
tokens = data.get("tokens_predicted", 0)
tps = data.get("timings", {}).get("predicted_per_second", 0.0)
return {
"req": i,
"tokens": tokens,
"latency": latency,
"tps": tps,
}
except Exception as e:
return {"error": str(e), "latency": time.perf_counter() - start}


def main():
print(f"Target: {URL}")
print(f"Model: {MODEL}")
print(f"Context: 65536 (q8_0 KV, parallel=2, 3 MIG instances), Concurrency: {CONCURRENCY}, Total requests: {TOTAL_REQUESTS}, max_tokens: {MAX_TOKENS}")
print("-" * 60)

results = []
errors = []
overall_start = time.perf_counter()

with ThreadPoolExecutor(max_workers=CONCURRENCY) as ex:
futures = {ex.submit(make_request, i): i for i in range(TOTAL_REQUESTS)}
for fut in as_completed(futures):
res = fut.result()
if "error" in res:
errors.append(res)
print(f"Request {res.get('req', '?')} ERROR: {res['error']}")
else:
results.append(res)
print(f"Request {res['req']:2d}: {res['tokens']:4d} tokens in {res['latency']:6.2f}s "
f"({res['tps']:6.2f} tok/s single-stream)")

overall_elapsed = time.perf_counter() - overall_start
total_tokens = sum(r["tokens"] for r in results)
avg_latency = sum(r["latency"] for r in results) / len(results) if results else 0
aggregate_tps = total_tokens / overall_elapsed if overall_elapsed > 0 else 0

print("-" * 60)
print(f"Completed requests: {len(results)}/{TOTAL_REQUESTS}")
print(f"Failed requests: {len(errors)}")
print(f"Total tokens generated: {total_tokens}")
print(f"Total wall-clock time: {overall_elapsed:.2f}s")
print(f"Aggregate throughput: {aggregate_tps:.2f} tokens/s")
print(f"Average latency per request: {avg_latency:.2f}s")


if __name__ == "__main__":
main()
Loading