Troubleshooting · diagnose by symptom

Start with the symptom, then gather the smallest useful evidence.

This page is a task-oriented diagnosis guide. It does not replace the Quickstart; use it when a command fails, /health is not green, performance changes unexpectedly, or the server is not using the model, adapter, or accelerator you expected.

Start with three probes

Binary

Pick the release artifact for your OS and accelerator: Linux/Windows CUDA builds for NVIDIA GPUs, Linux ROCm for supported AMD GPUs, Linux Vulkan for Vulkan-capable GPUs, or Apple Silicon Metal on macOS arm64.

Model path

Point Kiln at local Qwen3.5-4B weights with KILN_MODEL_PATH or model.path in TOML. The directory must contain the downloaded safetensors, configuration, and tokenizer files.

Health

After startup, ask /health what the server actually loaded before trying chat, SFT, GRPO, or adapter calls.

If the kiln CLI is on your PATH, run kiln health for the same info as a readable tree (use --json for scripts and --url http://host:8420 for remote servers). The curl commands below are the equivalent HTTP probes — handy for CI, scripts, or any environment without the CLI.

curl -fsS http://localhost:8420/health | jq .
curl -fsS http://localhost:8420/v1/models | jq .
curl -fsS http://localhost:8420/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"Qwen3.5-4B","messages":[{"role":"user","content":"Say hi."}],"max_tokens":16}' | jq .
Desktop App first launch

If the Desktop App does not finish first launch

The Desktop App wraps the same Kiln server, model path, GPU driver, and local port checks. If setup stalls, open the app's Logs view first, then compare the message with these common recovery paths.

  • If the server binary failed to download or verify, retry the download and confirm the app can write to its data directory.
  • If the model path is unset or missing weights, choose the local Qwen3.5-4B directory that contains safetensors, config, and tokenizer files.
  • If the CUDA driver is too old or an update is blocked on Linux or Windows, update the NVIDIA driver before launching the CUDA server.
  • If a Vulkan build falls back to CPU, run vulkaninfo --summary and confirm that the target GPU is listed before launching the Vulkan server.
  • If the port is already in use, stop the other Kiln/server process or change the Desktop App server port before restarting.
  • If Settings shows a load or recovery warning, review the preserved values and defaults before saving the repair. Automatic server launch stays off while the warning is unresolved; a newer settings schema requires a Desktop App update.
  • If the server enters a crash/restart loop, open Logs, fix the first setup error shown there, then restart from the app.
  • For app-specific paths and log locations, see the Desktop troubleshooting notes.

Wrong binary or GPU path

Symptom

The binary exits early, reports no usable accelerator, or starts with much lower performance than expected.

Check
  • Linux and Windows CUDA builds require an NVIDIA GPU.
  • The Linux ROCm release targets the GPU architectures declared in the release notes.
  • Linux Vulkan builds require a working Vulkan loader and compute-capable driver; vulkaninfo --summary should list the target GPU. Kiln negotiates the loader API up to Vulkan 1.2 rather than requiring 1.2 unconditionally.
  • CUDA release builds are compiled with CUDA 12.4; check the release notes for emitted SM targets.
  • macOS uses the Apple Silicon Metal artifact, not a CUDA or Vulkan artifact.
Fix
  • Download the matching artifact from GitHub releases.
  • Select Vulkan with accelerator.vulkan_device_index or its canonical environment override. auto prefers a discrete GPU; an unavailable explicit index fails instead of choosing another device.
  • The current memory-probe identity gate admits only index zero when exactly one relevant physical DRM device exists. Nonzero, multi-GPU, or remapped selections fail before model upload until backend selection and memory probing share a PCI address or UUID. This is an identity-safety gate, not a device or kernel allowlist.

Memory admission fails or Vulkan pauses under pressure

Symptom

Startup refuses a KV allocation, memory headroom unexpectedly becomes zero, or a Vulkan process previously paused or destabilized the host while allocating its cache.

Check
  • Inspect /v1/config at vram.live, including sample age, staleness, sampler health, and raw_observations.host_backed.
  • For Vulkan, inspect vram.vulkan_buffer_pool and /health at vulkan_buffer_pool. Growing retained bytes indicate recycler working-set growth; flat retention with growing RSS indicates a different ownership or page-residency mechanism. Route-specific misses identify device-local versus host-visible growth; host-visible lookup may legitimately hit a larger compatible idle bucket.
  • On Linux Vulkan, compare primary VRAM with the host-backed GTT tier. A large firmware VRAM carveout does not mean Linux can safely allocate an equally large host-resident KV cache.
  • If live ownership is flat but anonymous RSS grows, inspect AnonHugePages in /proc/<pid>/smaps. Repeated fixed-shape requests should reuse recently released KV blocks instead of continuously first-touching untouched pool pages.
  • Check host MemAvailable and every applicable cgroup memory.max/memory.high ancestor.
Fix

Reduce memory.num_blocks or memory.inference_memory_fraction in kiln.toml, reduce prefix_cache.max_entries, lower memory.vulkan_buffer_pool_gb, or disable the prefix cache. A zero Vulkan pool cap disables scratch retention; active operations may still need temporary memory. Free host memory or raise the deliberate cgroup limit. Do not bypass a missing, stale, or unhealthy sample: Kiln treats it as zero headroom so a zero-filled allocation cannot take down the process or host.

curl -fsS http://localhost:8420/v1/config | jq '.vram.live'
curl -fsS http://localhost:8420/health | jq '.gpu_memory.live, .vulkan_buffer_pool, .decode_runtime.memory_governor'
curl -fsS http://localhost:8420/metrics | grep -E 'kiln_gpu_memory_(probe_failed|sample_|sampler_)|kiln_gpu_host_backed_memory_bytes|kiln_vulkan_buffer_pool'

Inference pauses or throughput collapses at concurrency

Symptom

Streaming appears to pause between tokens, time to first token grows with queued prompts, or a wide CUDA/ROCm/Vulkan run stops scaling even though requests eventually complete.

Check
  • Inspect GET /v1/config at batching: require an active actor and rowwise_decode.enabled=false for ordinary throughput.
  • Compare the configured, backend-policy, effective, and source values for decode width and prefill_admission_quantum. Deterministic mode or token budget can clamp both.
  • Request include_performance=true and compare actor_queue_ms, actor_admission_ms, and actor_prefill_wall_ms with live actor, allocator, reclaim, and KV-resize metrics.
Fix

Put the four controls in [batching], restart, capture the resolved object, and change one field per A/B. Restore rowwise_decode=false for throughput. A smaller admission quantum can isolate prompt-admission delay but may reduce batch fill.

curl -fsS http://localhost:8420/v1/config \
  | jq '.batching, .decode_runtime.max_decode_batch'
curl -fsS http://localhost:8420/health \
  | jq '.decode_runtime.batching_configuration, .decode_runtime.batching_engine, .decode_runtime.memory_governor'
curl -fsS http://localhost:8420/metrics \
  | grep -E 'kiln_batching_engine_|kiln_gpu_memory_|kiln_kv_'

Do not label a scheduling pause as VRAM rebalancing without a matching reclaim, physical resize, synchronization, or memory-pressure event. A long actor queue/admission/prefill phase and a device-memory operation are different hypotheses and require different evidence. CUDA/Vulkan automatically admit up to effective decode width; ROCm/Metal/CPU default to 4, clamped to that width. Every batching change is process-lifetime policy and requires restart.

ROCm decode-width qualification Offline candidate selection, controls, and evidence boundary

ROCm decode-width selection

Use the committed width campaign as a bounded, offline autotuner, not a live response to a pause. It reruns the accepted control and tests wider candidates serially against one source-built release binary, using a fresh process, cooldown, and declared thermal guard for every arm. Each candidate must pass deterministic output, sampled traffic, post-sampling canary, memory, latency, graph, and cleanup gates. The selector favors the narrowest width near the best combined score; promotion also requires material gain plus a new exact-source soak and benchmark.

This is qualification-time autotuning, not online self-tuning: it never changes a live scheduler, never mutates this field, and stops wider arms after a correctness failure. Review the source-bound receipt, then pin the selected integer in server.max_decode_batch. A selected width applies only to the source, model, backend, device, and workload bound by that receipt.

CUDA and Metal backend-profile diagnosis Whole-profile comparisons, route attribution, Marlin, and training backward

CUDA backend-profile diagnosis

Inspect accelerator_runtime.cuda_kernel_profile before attributing a CUDA route change. native_default preserves the twenty-five established model/backend routes but is not itself a qualification claim; portable_fallback declines all twenty-five. The policy is immutable after startup and its source identifies TOML, canonical environment override, or default ownership.

curl -fsS http://localhost:8420/v1/config \
  | jq '.accelerator_runtime | {schema_id, version, cuda_kernel_profile, cuda_marlin_profile, cuda_flash_backward_mode}'
  • Use accelerator.cuda_kernel_profile = "portable_fallback" only as a whole-profile comparison, restart, and preserve both resolved config objects with the same model, request, seed, batch, and memory policy.
  • Do not use former CUDA-specific per-kernel environment switches. They are not aliases and cannot establish which route ran.
  • A difference between profiles localizes the problem to one of the twenty-five documented model/backend routes; it does not identify a particular route. Use route counters, profiler evidence, and a focused reproduction before drawing that conclusion.
  • The profile includes the documented forward and training routes, but is not a complete CUDA qualification and makes no claim about graph or Marlin behavior outside the architecture table.
  • For weight-layout regressions, use server.serving_profile = "experimental" and compare cuda_marlin_profile = "disabled" against "attention_mlp" before testing "attention_mlp_gdn". Restart between arms, record load time and resident memory, and include task-quality evaluation because W4A16 changes model weights.
  • For training backward replay, compare cuda_flash_backward_mode = "fast" and "deterministic" with identical source, shapes, seed, and optimizer state. This field does not make unrelated CUDA kernels deterministic.
  • Do not restore KILN_W4A16, KILN_W4A16_GDN_OUT_PROJ, KILN_DISABLE_PARALLEL_PACK, or KILN_FLASH_ATTN_BWD_DETERMINISTIC; they are not aliases.

Metal backend-profile diagnosis

Inspect accelerator_runtime.metal_kernel_profile before attributing a Metal route change. native_default preserves forty-five established native routes and leaves custom LM-head argmax off; portable_fallback declines all forty-six. The policy is immutable after startup and its source identifies TOML, canonical environment override, or default ownership.

curl -fsS http://localhost:8420/v1/config \
  | jq '.accelerator_runtime | {schema_id, version, metal_kernel_profile}'
  • Compare accelerator.metal_kernel_profile = "native_default" and "portable_fallback" only with identical source, model, request, seed, batch, memory, and graph policy; restart between runs and retain both resolved config objects.
  • A profile difference localizes the issue to the forty-six routes enumerated in the architecture reference. It does not identify a leaf; use route telemetry, a profiler trace, and a focused reproduction.
  • Do not restore former KILN_DISABLE_METAL_*, generic GDN/RMSNorm, or KILN_ENABLE_METAL_LM_HEAD_ARGMAX variables. They are intentionally not aliases.
  • Neither profile is a hardware-qualification claim. Record parity, memory, throughput, and lifecycle evidence on the exact pushed source before promotion.
Vulkan policy and quarantined-route diagnosis Capability-derived dispatch, benchmark interpretation, prefix reuse, and resident prefill

Vulkan kernel-policy diagnosis

kiln.vulkan-kernel-policy.v6 is derived once from the selected physical device's reported workgroup, shared-memory, descriptor, push-constant, subgroup, API, and memory-topology capabilities. Its former KILN_DISABLE_VULKAN_*, KILN_ENABLE_VULKAN_*, kernel-threshold, split-K, stage-profile, packed-weight, recurrence, and resident-decode variables are not controls and must not be used to explain a run. The backend, kernel, and resident-model planners share one source object for those decisions; malformed shell values, device names, vendor IDs, device IDs, and PCI identity cannot select a different path.

curl -fsS http://localhost:8420/v1/config \
  | jq '.accelerator_runtime | {schema_id, version, vulkan_kernel_policy_schema_id, vulkan_device_policy_schema_id, vulkan_device_index, vulkan_validation}'
  • Remove old kernel switches from launch scripts. They have no compatibility aliases or replacement fields.
  • Compare the binary/source commit and policy schema before comparing receipts. A route change requires a reviewed source revision and renewed Vulkan parity, performance, and soak evidence.
  • Use typed batching, memory, prewarm, and accelerator fields for supported operator decisions. Do not infer a hidden model-route or kernel override from a throughput change.
  • Historical optimization logs and the Vulkan microbenchmark document research experiments; neither defines the product runtime contract.
  • Select a device with typed accelerator.vulkan_device_index = "auto" or a strict zero-based index. Invalid explicit indices fail startup; they never fall through to another GPU.
  • Enable validation only with typed accelerator.vulkan_validation = true under the experimental profile. Startup fails when the Khronos validation layer is unavailable, so diagnostics cannot claim validation that was not active.
  • The serving/kernel product no longer reads residual KILN_VK_* tiling or CPU-fallback controls. The separate research executable uses only typed CLI arguments and cannot define a qualified product receipt.

Do not use the July 20 soak's 0.455 aggregate output tokens/second as a decode-rate diagnostic. That value divides all output by a 1,935-second mixed-concurrency request window; its 76.7 ms median inter-token latency corresponds to about 13.0 decode tokens/second. The benchmark page separates decode, prefill, TTFT, and request-window throughput and shows the July 27 regression and current correction as comparable runs.

Vulkan prefix-cache diagnosis

Vulkan currently correctness-quarantines all cross-request prompt, KV, and recurrent-state reuse. A conforming process reports the configured intent separately from an effective false capability and fresh-prefills exact repeats. Do not interpret zero hits as a tuning or capacity problem on this backend.

curl -fsS http://localhost:8420/v1/config | jq '.prefix_cache'
curl -fsS http://localhost:8420/health | jq '.prefix_cache, .decode_runtime.batching_engine.prefix_cache_enabled'
curl -fsS http://localhost:8420/metrics | grep '^kiln_.*prefix_cache'
  • Require effective_enabled=false, effective_reason="vulkan_correctness_quarantine", health prefix_cache.enabled=false, and actor prefix_cache_enabled=false.
  • Every lookup, hit, miss, hit-token, hit-block, cached-block, maximum-block, cached-entry, maximum-entry, state-byte, lease, and pending-release value must be zero.
  • Changing prefix_cache.max_blocks or max_entries cannot enable the cache on Vulkan. No request field or environment alias bypasses the source-level gate.
  • A nonzero activity value, an exact repeat that takes a cache route, or an effective true capability means artifact drift and requires stopping the process before trusting output.
  • Re-enablement requires production-model oracle parity across first use, exact repeats, strict descendants, changing concurrency, cancellation, and repeated history.

Vulkan resident-prefill diagnosis

The serving profile—not a device name or request field—controls admission. Stable and maintenance report resident_prefill_enabled=false; experimental reports true and may take the native route when the request and selected device satisfy its checks. This route is separate from cross-request prefix reuse, which remains quarantined on Vulkan in every profile.

curl -fsS http://localhost:8420/health \
  | jq '.decode_runtime.batching_engine | with_entries(select(.key | contains("resident_prefill")))'
curl -fsS http://localhost:8420/metrics \
  | grep '^kiln_batching_engine_.*resident_prefill'
  • Under stable or maintenance, every attempt, forward, decline, route-failure, row, completion, active-row, and batch-size field must remain zero; each request must report resident_prefill_used=false.
  • Under experimental, require the capability to be true before interpreting activity. A request-level resident_prefill_used=true means a native full-stack forward completed; it is route evidence, not a performance or correctness verdict.
  • There is no independent resident-prefill TOML field, environment switch, or request override. Change the serving profile, restart, and verify the resolved policy.
  • For wrong tokens, route failures, or instability, preserve the exact source/config/device receipt and return to stable. Production-model parity across changing cohorts, cancellation, and repeated process history is required before promotion.

CUDA graph capture is absent, unstable, or unexpectedly eager

Establish policy

Inspect the typed request before attributing behavior to capture. The default stable serving profile admits guarded live graph capture when the backend and request shape qualify; maintenance selects eager execution.

Check the bound

memory.cuda_graph_cache_entries is 1..=64, defaults to 8, and is fixed before device selection. Changing it requires restart; malformed, zero, and oversized values stop startup.

Do not bypass invariants

Stable paged metadata is mandatory and batched capture is unavailable. Former stable-metadata, batched-enable, no-replay, force-eager, and batched-KV environment probes are retired; setting them cannot alter CUDA decode.

curl -fsS http://localhost:8420/v1/config | jq '.cuda_graphs'
curl -fsS http://localhost:8420/health | jq '.decode_runtime.cuda_graphs'
kiln config --file kiln.toml

Use server.serving_profile="experimental" only for controlled qualification. A CUDA feature compile on a non-NVIDIA host is not runtime evidence. Re-enabling batched capture requires a source change plus real NVIDIA sanitizer, eager-parity, resilience, and throughput evidence; it cannot be promoted through configuration.

ROCm token pauses or irregular decode latency

Symptom

Token delivery pauses during an otherwise healthy decode, inter-token latency has a long tail, or a new decode shape pauses once and then runs normally.

Collect
  • Resolved accelerator, synchronization, and graph policy from /v1/config.
  • Before-and-after health and metrics snapshots around one reproducible request.
  • Request performance metadata, token timestamps, and matching server logs.
Interpret

A pause alone is not evidence of VRAM rebalancing. Require a matching allocator reclaim, physical KV resize, memory-pressure transition, or primary/host-backed memory change before using that diagnosis.

curl -fsS http://localhost:8420/v1/config \
  | jq '{policy: .accelerator_runtime,
         graphs: .rocm_graphs,
         graphs_reason: .rocm_graphs_unavailable_reason,
         phases: .rocm_graph_telemetry,
         phases_reason: .rocm_graph_telemetry_unavailable_reason}'
curl -fsS http://localhost:8420/health \
  | jq '.decode_runtime
        | {accelerator_runtime, rocm_synchronization, rocm_graphs,
           memory_governor, batching_engine}'
curl -fsS http://localhost:8420/metrics \
  | grep -E '^kiln_rocm_(cleanup|synchronization|graph_)|^kiln_gpu_memory_|^kiln_kv_'

Read each signal at its boundary

  • waited_ns measures host wall time where Kiln observed unfinished GPU work. It does not identify which earlier kernel consumed that time.
  • Use reason deltas to distinguish external yield, output handoff, matmul/cast, graph transition, memory reclaim, and recovery boundaries.
  • A live graph phase or a new capture, admission, eviction, byte-budget rejection, or fallback around the pause supports a graph-lifecycle hypothesis.
  • A null graph snapshot means unavailable data, not an empty cache. Read the paired unavailable reason and the independent phase-telemetry authority.
  • Graph capture success and cache admission are different outcomes: a successfully launched candidate can still be rejected safely by the final cache-admission check.

Stop on a safety fault

cleanup_quarantined=true, active synchronization-telemetry loss, a capture parity failure, or a replay failure is not a tuning result. Stop the arm, preserve config, health, metrics, and logs, then restart the process. Quarantine is process-lifetime state; a later diagnostic drain does not make execution safe again.

Run one-variable comparisons Synchronization, graph lifecycle, cache, and rollback

Synchronization

Compare legacy_host_barriers with stream_ordered only under the experimental profile. Use the same binary, model, corpus, request order, seed, sampling, batching, memory budget, concurrency, and warmup. Run the arms serially in fresh processes and disable graphs in both arms so graph capture is not a second changed variable.

# control.toml
[server]
serving_profile = "experimental"

[accelerator]
rocm_synchronization_mode = "legacy_host_barriers"
rocm_graph_mode = "disabled"

# candidate.toml is identical except:
# rocm_synchronization_mode = "stream_ordered"

Compare token or logit parity, completion count, throughput, p50/p95/p99/max inter-token latency, reasoned wait/skip deltas, memory peaks, and post-run plateaus. legacy_host_barriers remains the qualified default. A lower wait count without parity and stability is not a pass.

Graph lifecycle

ModePurposeProfile
disabledEager control with no graph warmup, capture, or replay.Any
warmup_then_eagerGraph-shaped warmup without native capture.Experimental
lazy_capture_replayLazy native capture and replay for eligible shapes.Experimental

Compare disabled with graph-shaped eager before testing native capture. Keep synchronization, cache bounds, corpus, concurrency, and memory policy fixed. A one-time pause accompanied by a new successful capture can be a bucket transition; repeated fallbacks, replay failures, parity failures, or an unbounded memory plateau are not.

Prefix cache

If deterministic output still differs after graph capture and physical KV autoscaling are off, compare prefix_cache.enabled=true with false in otherwise identical files. The disabled arm must report no lookup, hit, retained-block, lease, or recurrent-state activity. This isolates storage and reuse only when both arms preserve the same prompt-chunk geometry.

Rollback

[server]
serving_profile = "stable"

[accelerator]
rocm_synchronization_mode = "legacy_host_barriers"
rocm_graph_mode = "disabled"

Validate, restart, and require effective graph mode disabled. Stable accepts explicit diagnostic backend settings, so the rollback file states both the portable synchronization mode and eager graph mode directly; the resolved policy reports those exact choices.

Use the latency observability guide for metric definitions, the serving benchmark protocol for comparable runs, and hardware qualification for source-bound receipts. A receipt describes one build, device, driver, model, and workload; it never becomes product routing.

ROCm long prefill pauses or runs out of memory

Symptom

A long ROCm prompt pauses before its first token, a native training step stalls at a long sequence, or a prefill fails with an allocation/OOM error. Short prompts may be unaffected because automatic tiled prefill starts at an inclusive 256-token crossover.

Check
  • Inspect /v1/config.streaming_prefill and health prefill_runtime.streaming_prefill; require identical dispatch, tiles, sources, and restart flags.
  • On ROCm defaults, expect auto dispatch at >=256, base/tape tiles of 256, and detached/boundary/replay tiles of 8192.
  • Require server.max_prefill_tokens_per_cycle=256 and server.max_batch_tokens=512 for that tile plus the effective decode width. Startup rejects a mismatched actor-prefill contract before loading model weights.
  • Separate ordinary inference/base tiles, tape-authoritative training tiles, and detached materialized full-attention routes. Tuning the wrong field cannot explain that path.
  • Correlate the gap with actor phase timing, model segment/tile timing, external-yield synchronization, allocator reclaim, physical KV resize, live memory samples, and primary versus host-backed peak memory.
Fix or isolate

Keep actor ownership and tiled dispatch fixed. Change the actor ceiling, streaming threshold, and base tile together in a separate diagnostic file so they retain one numerical boundary. Reduce that boundary in multiples of 64 for a measured OOM; do not broadly raise the timeout or infer VRAM rebalancing from a temporal gap alone.

Exact tiled-prefill diagnostic and policy defaults
# kiln-rocm-prefill-128-diagnostic.toml
[server]
max_batch_tokens = 256
max_prefill_tokens_per_cycle = 128

[streaming_prefill]
mode = "enabled"
threshold_tokens = 128
tile_tokens = 128
tape_tile_tokens = "auto"
detached_full_attn_tile_tokens = "auto"
last_token_lm_head = true

kiln config --file kiln-rocm-prefill-128-diagnostic.toml --backend rocm
kiln serve --config kiln-rocm-prefill-128-diagnostic.toml
curl -fsS http://localhost:8420/v1/config | jq '.streaming_prefill'
curl -fsS http://localhost:8420/health | jq '.prefill_runtime.streaming_prefill'
curl -fsS http://localhost:8420/metrics \
  | grep -E 'kiln_batching_engine_|kiln_gpu_memory_|kiln_kv_'

On ROCm, mode="disabled" and a threshold later than the first actor tile are invalid: startup rejects either before loading weights. In the diagnostic arm, the explicit base tile is inherited by both specialized auto fields and by detached boundary/replay variants. Set tape_tile_tokens or detached_full_attn_tile_tokens explicitly when you need a route to differ. Every value is immutable after startup; changing the file or shell without restart has no effect. A smaller tile can change deterministic output, so require parity before treating it as a fix. Canonical names follow KILN_STREAMING_PREFILL_<FIELD>. Deprecated shorter names parse strictly, warn, and fail startup if they conflict with a canonical value.

Backend auto policy starts at 256 on ROCm, 2048 on CUDA/Metal, and never on CPU/Vulkan. Base/tape defaults are ROCm 256, CUDA 1024, Metal/Vulkan 2048, and CPU 8192. Detached defaults are 8192; CUDA boundary and replay use 65536. Forcing enabled on CPU or Vulkan is an explicit diagnostic, not proof that the route is supported or faster. Qualify every backend on the exact target device and source revision. Compile-only or hosted CI without the declared accelerator is not hardware evidence.

Tape switches do not isolate training or inference

Symptom

An old launch script sets KILN_USE_TAPE_*, ordinary inference unexpectedly avoids a fast path, a workload reports an unavailable tape/backward route, a step fails with an exact LoRA gradient error, or a profile shows frozen base-weight gradients or dWeight GEMMs.

Check
  • Inspect /v1/config at training.optimizer_support.workloads. That per-workload result, not a shell variable, is the static training-substrate authority.
  • For an inference pause, use actor, prefill, synchronization, and memory metrics. A historical tape variable is not evidence that a tape scope exists.
Training graph and gradient checks
  • Search the service definition for KILN_USE_TAPE_FORWARD, KILN_USE_TAPE_FLASH_ATTN, KILN_USE_TAPE_SDPA, KILN_USE_TAPE_LORA_ADD, and the four KILN_USE_TAPE_GDN* names.
  • Read the complete gradient error. A missing registered input indicates a disconnected use even if another clone reached the same leaf. Missing or unknown leaf IDs indicate graph/deposit drift, including a temporary slice ID that was not assembled into its original full leaf. Shape, backward dtype, or master-device mismatches indicate producer drift; NaN or infinity indicates numerical failure. A finite all-zero gradient is accepted, and a checkpoint range with no configured leaves must return an empty set.
  • Inspect tape inputs and profiler shapes. Only LoRA A/B are trainable model leaves. Embedding tables, base projection matrices, normalization weights, GDN gate parameters, MTP projections, and loss heads must be saved constants. Their backward routes must not emit a frozen-weight gradient; split Q/gate chunks must deposit under the original full A/B IDs and retain tape-aware reshapes.
  • On Metal, a synchronized full-gradient host scan at the optimizer boundary is the current correctness fallback until a native finite reducer is qualified. Profile it as an expected synchronization cost; do not bypass the check.
Fix

Delete all eight names from launch scripts; they were removed without aliases or replacement fields. Kiln activates its required tape solely through an internal training scope. Ordinary inference opens no such scope and retains forward-only paths, including GDN chunkwise recurrence and CUDA’s weight-aware embedding lookup. Fix the producer named by an exact-gradient rejection. If a frozen gradient appears, route that operation through its frozen-input backward rather than filtering the result at the optimizer. No configuration bypass is supported.

curl -fsS http://localhost:8420/v1/config \
  | jq '.training.optimizer_support.workloads'

env | grep -E '^KILN_USE_TAPE_(FORWARD|FLASH_ATTN|SDPA|LORA_ADD|GDN)'

Debug or performance tuning may choose only graph-preserving implementations while the internal scope is active; it must not sever the gradient graph or change the update rule. The source contract that enforces scope ownership is portable static evidence, not hardware qualification. Validate numerical correctness, latency, memory, cancellation, and stability on the exact target device and source revision before making a backend claim.

Model weights are not found

Symptom

Startup fails with a missing model path, missing tokenizer, or missing safetensors message.

Check
  • The model directory exists on the same machine or inside the Docker container.
  • The path contains Qwen3.5-4B weights, config, and tokenizer files.
  • Relative paths are resolved from the current working directory.
Fix

Set the path explicitly. For Docker, mount the host directory at the same path you pass to the server.

KILN_MODEL_PATH=/models/Qwen3.5-4B ./kiln serve

# Or put `path = "/models/Qwen3.5-4B"` under [model] in kiln.toml:
./kiln serve --config kiln.toml

/health is not green

Symptom

The HTTP server is reachable, but chat or training requests fail.

Check
  • /health reports the configured model path and device state.
  • /v1/models returns the model id you expected.
  • The listen address is localhost:8420 unless you changed it.
Fix

Fix the first failing health field before debugging chat payloads. Health output is the fastest way to separate setup issues from request-shape issues.

Remote server is not reachable

Symptom

Kiln is running on a GPU box, Tailscale host, or reverse-proxied machine, but client commands fail with a connection error.

Check
  • The default bind is local-only: 127.0.0.1:8420.
  • For private-network access, set server.host = "0.0.0.0" in config or start with KILN_SERVER_HOST=0.0.0.0.
  • Only expose that bind on a trusted/private network or behind a reverse proxy that adds authentication.
  • From the client machine, verify the exact host with curl -fsS http://gpu-box:8420/health.
Fix

Open the firewall or private-network route to the server port, then point CLI client commands at the same base URL.

kiln health --url http://gpu-box:8420
kiln train status --url http://gpu-box:8420
kiln adapters list --url http://gpu-box:8420

Older-release long-prefill and tool-call timeouts

Symptom

On kiln-v0.2.9, long tools-bearing chat completions or long-prefill prompts could time out under concurrent load, then leave later requests failing until restart.

Check
  • This guidance is only for users intentionally pinned to kiln-v0.2.9.
  • Closed issue #664 documents the tools-bearing cascade after a prefill timeout.
  • Closed issue #656 documents the KV-cache-exhaustion and prefill-state-cleanup investigation.
  • Closed issue #686 documents repeated long-prefill HTTP 408s around the old timeout boundary.
Fix

Upgrade from v0.2.9 to a current release. If pinned to v0.2.9, run clients with workers=1 so requests are serialized; if concurrent workers are required, set server.request_timeout_secs to at least 600.

The workers=1 mitigation is client-side serialization, not a kiln server flag: keep at most one in-flight request in your driver, worker pool, or load generator. It avoids concurrent prefills, which is the condition that exposed the older degraded-state failure mode.

The request_timeout_secs >= 600 mitigation is also historical. It was useful for pinned kiln-v0.2.9 deployments that had to keep workers=2 while running tight KV-cache caps and roughly 20k-token-or-longer prefills. Current releases route prefix-cache prefill through the tiled/streaming dispatcher and include follow-on prefix-cache memory, KV auto-sizing, streaming-prefill, and observability fixes.

For the original diagnosis and bisect notes, read the issue 686 investigation.

Mock mode is not real training

Symptom

A training request appears to complete, but inference quality does not change or no real adapter weights appear.

Check
  • Mock mode is for API-shape and UI checks only.
  • Real /v1/train/sft and /v1/train/grpo jobs require a loaded model and usable accelerator.
  • /v1/train/status should show completed work for the adapter name you sent.
Fix

Use mock mode to verify wiring, then run the payload against a real model under a profile that admits training. Treat training endpoints as privileged: a successful job saves adapter weights and may activate them when auto_load is true and any held-out post-eval gate passes.

An OpenEnv run cannot start, stalls, or fails verification

Symptom

Discovery fails; admission rejects reset options; collection remains capacity-waiting; final status reports environment_identity_changed; retained data reaches its aggregate budget; artifact download reports openenv_artifact_integrity_failed; slow policy inference loses a socket; or kiln openenv verify reports identity, digest, reward, or reset-plan drift.

Check
  • Run kiln openenv inspect --environment URL, then inspect kiln openenv status RUN_ID and server metrics.
  • For a failed run, read failure.code, failure.stage, failure.retryable, and failure.hint. Exact protocol_code or http_status evidence appears only when the environment supplied it.
  • environment_identity_changed at identity_verification means stable discovery changed between initial inspection and the post-episode revalidating phase. Kiln published none of that attempt’s episodes.
  • Use one shared reset object, or exactly one environment_reset_options object per URL; groups must cover every endpoint.
  • Protected origins need an exact-origin credential; remote server runs require explicit operator policy and HTTPS.
  • Inspect server logs for WebSocket timeouts or unsolicited frames. Kiln pumps Ping/Pong while inference is pending and sends periodic read-only state exchanges, then poisons ambiguous lock-step sessions.
  • If thinking-on rollouts end as invalid_model_action, inspect whether max_action_tokens truncated the reasoning before a final schema-valid answer. Increase the action budget; disable thinking only for an intentionally final-action-only policy.
  • A retained-representation budget error reports current and additional bytes. Reduce group size, concurrency, steps, action size, or observation size; Kiln publishes no partial artifact bundle.
  • An artifact integrity failure means the manifest byte count or SHA-256 no longer matches the bounded regular file. Restore the original bundle or recollect; never edit retained artifacts in place.
Fix

Correct the URL, credential, aligned reset plan, or group count. Pin or stabilize an environment deployment that changed identity, inspect it again, and recollect. Reduce concurrency or raise the bounded capacity wait when the environment reports saturation. retryable=true authorizes a new attempt after the stated correction, never in-place episode resume; use a new idempotency key because a retained key resolves to its original run. Keep dataset, replay, and summary together; a verification mismatch means restore the original bundle or recollect—do not edit the receipt. Then use live replay only after offline verification passes.

See the OpenEnv training guide for protocol and workflow details, and OpenEnv replay and recovery for exact mismatch semantics.

Adapters are in a different directory than expected

Symptom

/v1/adapters does not list an adapter you trained, uploaded, or expected from a previous run.

Check
  • When model.adapter_dir is omitted, Kiln uses <model.path>/adapters.
  • Docker containers only see directories you mounted.
  • Different working directories can imply different relative adapter paths.
Fix

Set model.adapter_dir in TOML or KILN_MODEL_ADAPTER_DIR before startup, mount that exact directory in Docker, restart, and verify saved plus active state with GET /v1/adapters.

An adapter mutation or training publication was rejected

Symptom

Delete returns HTTP 409 adapter_active or adapter_loaded, or a training job fails with adapter_revision_conflict.

Check
  • GET /v1/adapters reports both the server default and exact loaded_adapter_identity.
  • GET /v1/train/status/{job_id} keeps the complete publication failure in error.
  • A post-eval gate cannot safely rewrite a physically loaded adapter under the same name.
Fix

For adapter_active or adapter_loaded, call POST /v1/adapters/unload and retry. For adapter_revision_conflict, keep the intervening winner and resubmit against the current revision. Unload a gated same-name target or choose a versioned config.output_name. Kiln removes hidden staging directories during normal cleanup.

Where to go next