config: fix UTF-8 decode crash + null config-value TypeError on Windows (doctor/resource_plan) (#184)

Two related crash bugs in the Python planning layer:

P1: Path.read_text() without encoding= defaults to locale.getpreferredencoding()
    (cp1252 on most Windows installs). HuggingFace config.json is UTF-8 — if it
    contains any non-ASCII byte (Chinese fields, accented chars, emoji in
    metadata), read_text() raises UnicodeDecodeError. In doctor.py this is
    caught and mis-reported as 'config.json is missing or invalid' (false
    negative); in build_plan() called directly it is an uncaught crash.

    Fixed: read_text(encoding='utf-8') in both resource_plan.py and doctor.py.

P2: int(cfg.get('kv_lora_rank', 0)) crashes with TypeError if the key is
    present but null (JSON null). dict.get() returns the default only when
    the key is ABSENT; a null value returns None, and int(None) raises
    TypeError. The engine validates against malformed configs in C but the
    Python planner did not.

    Fixed: int(cfg.get(key) or 0) — coerces both missing and null to 0.
    Applied to all 8 int(cfg.get(...)) calls in build_plan().

Co-authored-by: woolcoxm <13604288+woolcoxm@users.noreply.github.com>
This commit is contained in:
woolcoxm
2026-07-14 08:17:42 -04:00
committed by GitHub
parent 39dcebf39a
commit 35ed9bc921
2 changed files with 8 additions and 8 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ def run_doctor(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0, *,
config = model / "config.json"
try:
valid_config = isinstance(json.loads(config.read_text()), dict)
valid_config = isinstance(json.loads(config.read_text(encoding="utf-8")), dict)
except (OSError, ValueError):
valid_config = False
checks.append(_check("model.config", "pass" if valid_config else "fail",
+7 -7
View File
@@ -39,7 +39,7 @@ def analyze_model(model):
config_path = model / "config.json"
if not config_path.is_file():
raise ValueError(f"missing config.json: {model}")
config = json.loads(config_path.read_text())
config = json.loads(config_path.read_text(encoding="utf-8"))
shards = sorted(model.glob("*.safetensors"))
if not shards:
raise ValueError(f"no safetensors shards: {model}")
@@ -210,15 +210,15 @@ def build_plan(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0,
if ram_budget < 4 * GB:
ram_budget = 8 * GB
typical = info["typical_expert_bytes"]
layers = int(cfg.get("num_hidden_layers", 0)) + 1
kv_bytes = layers * context * (int(cfg.get("kv_lora_rank", 0)) +
int(cfg.get("qk_rope_head_dim", 0))) * 4
kv_buffer = context * int(cfg.get("num_attention_heads", 0)) * (
int(cfg.get("qk_nope_head_dim", 0)) + int(cfg.get("v_head_dim", 0))) * 4
layers = int(cfg.get("num_hidden_layers") or 0) + 1
kv_bytes = layers * context * (int(cfg.get("kv_lora_rank") or 0) +
int(cfg.get("qk_rope_head_dim") or 0)) * 4
kv_buffer = context * int(cfg.get("num_attention_heads") or 0) * (
int(cfg.get("qk_nope_head_dim") or 0) + int(cfg.get("v_head_dim") or 0)) * 4
runtime_bytes = int(1.2 * GB + 2.5 * GB + 64 * typical + kv_bytes + kv_buffer)
cache_bytes = max(0, ram_budget - info["dense_bytes"] - runtime_bytes)
per_cap = info["per_cap_bytes"]
configured_experts = int(cfg.get("n_routed_experts", 0))
configured_experts = int(cfg.get("n_routed_experts") or 0)
cap = int(cache_bytes // per_cap) if per_cap else 0
if configured_experts:
cap = min(cap, configured_experts)