jcanizalez/qwen3-1.7b-k8s-alert-triage-lora
Qwen3-1.7B Kubernetes alert triage (LoRA)
A LoRA adapter for Qwen3-1.7B that reads one Kubernetes alert plus the state of its namespace and names the fault that caused it. It is small enough to run on two CPU cores inside the cluster it watches, so alerts, logs and topology never leave it.
On 124 held-out alerts it scores 82.3% accuracy (a second training run of the same recipe scored 79.8%), against 67.7% for Claude Opus 5 and 60.5% for Gemini 3.5 Flash-Lite given the same prompt.
Code, dataset harness and every result: github.com/jcanizalez/k8s-alert-triage
Labels
bad_image_tag, crashloop_bad_command, dependency_scaled_to_zero, dns_broken, init_container_failing, liveness_probe_failing, missing_configmap, missing_secret, readiness_probe_too_strict, resource_quota_exceeded, unschedulable_resources, wrong_service_selector, and none for routine noise.
Prompt
The model was trained on chat messages with thinking turned off. The system message lists the labels; the user message is the alert followed by the namespace's pods, services and recent events, built by prompt_for in eval/baseline.py. Use that function, or match its format exactly:
ALERT: ServiceHasNoEndpoints
severity: critical
namespace: demo
object: frontend
summary: Service frontend has no ready endpoints
description: ...
PODS:
frontend-554f685c79-tlhjb: phase=Running ready=True restarts=0 waiting=[] lastTerminated=[]
SERVICES:
frontend: type=ClusterIP selector={'app': 'nothing-matches-this'}
RECENT EVENTS:
Warning Failed frontend-576f985f7b-cjvfv: Error: ImagePullBackOffThe answer is one label, for example wrong_service_selector.
Use it
With transformers and PEFT:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-1.7B", torch_dtype="auto")
model = PeftModel.from_pretrained(base, "jcanizalez/qwen3-1.7b-k8s-alert-triage-lora")
tok = AutoTokenizer.from_pretrained("jcanizalez/qwen3-1.7b-k8s-alert-triage-lora")
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
ids = tok(prompt, return_tensors="pt")
out = model.generate(**ids, max_new_tokens=48, do_sample=False)
print(tok.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True))For llama.cpp, merge the adapter into the 16-bit base and convert to GGUF at q8_0. Serve with --reasoning off so the template matches training.
Do not quantize to 4 bits. The same adapter scored 82.3% loaded, 80.6% merged at 16 bits, 77.4% as a q80 GGUF and 30.6% as q4k_m.
Training
- LoRA r=16, alpha=16, dropout 0, on all seven projections of every layer: 17,432,576 trainable parameters, 1.00% of the model.
- 16-bit base (not QLoRA), so merging is exact.
- 302 training alerts from 156 injection windows in a kind cluster, split by injection window so no incident appears in both training and test.
- 3 epochs, learning rate 2e-4, effective batch 8, AdamW 8-bit, linear schedule, seed 7. About 9 minutes on a free Colab T4 with Unsloth.
Limits
Trained on one small demo app in one local cluster, with twelve faults injected on purpose. It knows that cluster's failures, not yours: retrain it on your own alerts before relying on it. It is weakest at dns_broken and at recognising routine noise, where a larger model does better, so escalate what it is unsure of.
