Files
club-3090/docs/PULL_EMIT_DERIVED.md
noonghunna b327f0311a refactor(gemma): rename dual slugs + ladder gemma-bf16-mtp to 131K (#286)
Rename vllm/gemma-int8→gemma-int8-mtp + vllm/gemma-mtp→gemma-bf16-mtp (both carry MTP n=4; only KV format differs); kv-calc alias fix. Ladder gemma-bf16-mtp default 32K→131K (measured BF16 KV pool 196,527 tok @ 0.95; verify-stress 8/8, NIAH→120K, bench 118.8/154.3, zero TPS regression). Codify the dual-card max-context-priority rule in DUAL_CARD.md. Suite 41/41.
2026-06-01 01:20:46 +05:00

24 KiB
Raw Blame History

Pull-Emit-Derived — the [E] stage of scripts/pull.sh (v0.8.0, #147)

Contributor/maintainer guide for the v0.8.0 Pull-Emit-Derived stage. The Pull-Gate (docs/PULL_GATE.md) evaluates any safetensors HF repo and pull-and-emits curated (Tier-1) models only. [E] is the deferred extension that makes a download-eligible derived (non-curated) model real: it emits a patchless default-vLLM compose, downloads the weights, boots them, smokes the endpoint, and writes the §6 capture artifacts the later Loop phase consumes.

[E] is post-Pull-Gate, not a new gate: it runs as a stage inside run_pull(), strictly after a [C1] terminal, and never alters the locked 6-stratum taxonomy, the [C1] table, or curated Path-A behaviour. Those are consumed, never modified.

The authoritative spec is the test, not this doc

Per the locked v0.8.0 stop-condition, the executable specs are authoritative. For [E] those are scripts/tests/test-generate-from-profile.sh (CONTRACT-2/5: the derived template + the eligibility gate), scripts/tests/test-pullgate-download.sh (CONTRACT-3: the download stage), scripts/tests/test-pullemit-capture.sh (CONTRACT-4: the §6 capture emitters + manifest), and the [E] continuations / trigger matrix in scripts/tests/test-pull.sh (g16g22). Where this prose and any of those tests ever disagree, the test is correct; fix the doc. This document is the explanatory companion.

For the curated [D] substrate [E] reuses for the engine-pin resolver and the structured-refusal style, see docs/COMPOSE_GENERATOR.md; for the patch/arch/ profile data model, docs/PATCH_ATTRIBUTION.md and docs/PATCH_POLICY.md.

On-rig status: the mocked suites listed above are green and are the shipped E1E4 contract. The real small-non-curated end-to-end run (pull <slug> --profile-like <vLLM key> → derive → gates → [C1] → emit → download → boot → smoke → teardown) is pending on-rig validation (E5) and is driven separately. Nothing below describes the on-rig run as validated; it describes the shipped, unit-verified E1E4 behaviour.


1. What [E] adds on top of Pull-Gate

Class of model Pull-Gate behaviour With [E]
curated (Tier-1), Path A full gate → stratum-6 [D] dry-run → real [D] emit of the validated registry key; no download stage (curated weights are local) unchanged — curated Path-A is [D]-emit only, byte-for-byte. [E] never runs for a curated slug.
derived (non-curated), Path B full gate → print the §7-caveated verdict → STOP (structurally incapable of emit/download) a download-eligible derived verdict now continues into [E]: CONTRACT-5 eligibility gate → generate_from_profiledownload_modelboot_derivedsmoke_derivedemit_capture (+ pt5 on override)

[E] is wired into run_pull() (scripts/lib/profiles/pull.py) from exactly two post-[C1] return points, and only when not is_curated and not dry_run:

  1. the satisfied proceed / confirm→proceed Path-B verdict, and
  2. the override-accepted terminal that --force-download satisfied.

The [E] stage mutates only the additive PullResult fields (download_ok, boot_ok, smoke, capture_paths) plus notices/diagnostics. ok / terminal / raw_verdict / emitted / stratum / abort_reason are left exactly as the [C1] decision set them, so every pre-[E] test-pull.sh assertion is byte-unaffected. Every stage function is injectable; all stage exceptions are caught and recorded structurally — the [E] stage never raises out of run_pull().


2. Trigger semantics (exactly as shipped)

This is the new user-facing contract. It supersedes the pre-[E] "Path B never emits/downloads" / "--force-download is a NO-OP" rules — but only for the non-curated derived download-eligible no---dry-run case. (scripts/tests/test-pull.sh g16g22 is the truth table.)

Run shape Enters [E]? Behaviour
non-curated + download-eligible [C1] terminal (proceed / satisfied confirm→proceed) + no --dry-run yes CONTRACT-5 gate → emit → download → boot → smoke → capture (pt14 + manifest). is_override_accepted=False (no pt5).
non-curated + override-accepted (the terminal --force-download satisfied) + no --dry-run yes same chain, plus pt5 override force-capture (is_override_accepted=True). --force-download activates [E] only for this terminal (it was a no-op pre-[E]).
--dry-run (any slug) no verdict-only, unchanged. --dry-run never enters [E]. A curated slug only reaches Path B via --dry-run, which this guard excludes.
curated slug (Path A) no curated Path-A [D]-emit, unchanged. Curated weights are local — there is no download stage.
non-curated, confirm→proceed without --yes; or low-confidence wont-fit without --force-download no honest non-pass ([C1] not satisfied); [E] is gated behind a satisfied download-eligible terminal.

override-accepted is not a fit. The §5.3 rule is preserved verbatim in the notices: a forced low-confidence download is a calibration signal, never recorded as fit-validated. On the override-accepted [E] path the pt5 force-capture (§4 below) carries the literal calibration_signal_not_validated:true.

If the run is download-eligible non-curated but the local GPU topology cannot be determined (no nvidia-smi, no --hardware-gpus override), [E] does not run — it refuses honestly (gpu-topology-undetermined notice) rather than fabricate a topology; the Pull-Gate verdict stands.


3. CONTRACT-5 — the derived-emittable eligibility gate

generate_compose.derived_emittable(einput) -> (bool, reason|None) is a pure pre-[E] precondition run before any emit/download. It is not a new gate state — it is a structured-refuse precondition, printed like Pull-Gate's honest non-answer terminals. On refuse: a derived-runtime-unsupported:<reason> notice + diagnostics, no download, no boot.

The derived template is deliberately patchless (no Genesis / curated overlays). Many curated profiles only work because of those overlays, so inheriting an arbitrary --profile-like runtime shape into a patchless compose would be a guaranteed-broken boot. CONTRACT-5 rejects, by construction, anything that would need a patch/overlay/Genesis or that the local hardware cannot run. A model is derived-emittable iff all hold (checked in this short-circuit order; first failing clause names the reason):

Clause Refuse reason token Property checked
engine clean + Docker-image install derived-runtime-unsupported:engine-install-method the resolved engine YAML must have required_genesis == false and vendored_overlays == [] and install.method == "docker_image". Property-driven (not a hard engine list) so it is future-proof; an unresolvable engine YAML also fails here. (vllm-stable/vllm-stable-next are clean but install.method: pip — this clause keeps them out so the gate never emits an invalid image: vllm==....)
no overlay features derived-runtime-unsupported:overlay-feature the --profile-like COMPOSE_REGISTRY runtime entry has required_engine_features == [] (the registry runtime entry, not the engine YAML).
KV in the explicit safe set derived-runtime-unsupported:kv kv_format ∈ {bf16, fp16, fp8_e5m2, fp8_e4m3} (the literal DERIVED_SAFE_KV set). This is not "kv_arg() returns a defined result" — kv_arg() is also defined for int8_per_token_head / turboquant_3bit_nc, the values that must be rejected, so that predicate is self-contradictory. kv_arg() is emission-only (see §4 / §5).
no drafter derived-runtime-unsupported:drafter runtime["drafter"] is None — no DFlash / MTP / speculative / drafter (those need patches [E] does not include).
local GPUs can run this tp derived-runtime-unsupported:gpu-count visible_gpu_count >= runtime["tp"] and len(selected_gpu_indices) == tp. A clean no-drafter TP=2 shape on a 1-GPU rig is rejected here, before a multi-GB download.
quant resolvable derived-runtime-unsupported:unsupported-quant-for-derived[:autoround|:missing-torch-dtype] weight_format must be resolvable by the CONTRACT-2 dispatch table (§4). autoround is an explicit reject (:autoround); a quantized repo with no usable compute dtype is :missing-torch-dtype.

derived_emittable() is called twice on the success path — once by run_pull() as the pre-[E] precondition, and once again as the first thing generate_from_profile() does (it raises Refuse(reason), mirroring [D]'s structured-refusal style, on any reject) so the emitter can never be called past a reject. The patchless --kv-cache-dtype / no-patch rows of the derived template are therefore safe by construction.

scripts/tests/test-generate-from-profile.sh carries the negative matrix: vllm/gemma-int8-mtpoverlay-feature; a TQ3-KV shape → kv; a drafter/MTP shape → drafter; a clean TP=2 shape on a simulated 1-GPU einputgpu-count — each asserting no download and no emit, plus one positive clean/no-drafter shape that proceeds.


4. The derived-vllm template + quant/dtype dispatch (CONTRACT-2)

generate_compose.generate_from_profile(root, einput) -> (compose_text, meta) is an additive entry point. It emits a separate derived-vllm base template — not the curated compose_service_template. (The curated template hardcodes Qwen-specific constants — --quantization auto_round, --reasoning-parser qwen3, the Qwen chat template, --tool-call-parser qwen3_coder — so reusing it for an arbitrary derived model would emit a broken boot.) The registry-key generate(profile) path is byte-unchanged (test-generate-compose.sh byte-identical-proven, the P3 precedent).

Per-arg policy of the derived template (as shipped):

Arg / field Derived-template source
image the resolved engine.install.spec from the clean engine YAML — a pinned image string, recorded in the capture manifest. Not a launch-time ${VLLM_NIGHTLY_SHA} expansion ([E] boot does not go through launch.sh; an unresolved image risks a wrong-image boot).
--model the container path /models/club3090/pulls/<slug-sanitized>. The host dir <HF_HOME>/club3090/pulls/<slug-sanitized>/ is bind-mounted :ro to that container path via a host:container:ro volumes: entry. Not ${MODEL_DIR}, not the HF hash-cache snapshot path.
--served-model-name the slug sanitized: lowercased, every non-[a-z0-9._-]-, collapse repeats, trim/strip, capped to vLLM's name limit.
--quantization / --dtype the CONTRACT-2 dispatch table below.
--tensor-parallel-size / --max-model-len / --gpu-memory-utilization / --max-num-seqs / port from einput.runtime (the --profile-like shape) — faithful inheritance. If the profile's --max-num-seqs is unsafe for the derived model, [B] kv-calc already prices it and the on-rig run catches OOM (pending E5).
--kv-cache-dtype kv_arg(kv_format) is emission-only here, mapping the already-eligible value (CONTRACT-5 already gated it): bf16/fp16 → flag omitted, fp8_e4m3fp8, fp8_e5m2fp8_e5m2.
NVIDIA_VISIBLE_DEVICES (env) exactly einput.selected_gpu_indices — the same GPUs [B] priced. Never unset/all for derived.
--trust-remote-code emitted only if the [C0] trc gate already resolved permitted (signalled on einput.diagnostics["trc_permitted"] is True); absent / not-True → never emitted.
--chat-template, --reasoning-parser, --default-chat-template-kwargs, --enable-auto-tool-choice, --tool-call-parser not emitted — curated-specific. Derived uses vLLM defaults; derived tool/chat-template/reasoning support is explicitly OUT of scope.
patch insertion points patchless — no Genesis / curated patches (CONTRACT-5 guarantees the runtime never needs them).

Quant/dtype dispatch table (_QUANT_DISPATCH):

der.profile.weight_format --quantization --dtype
awq awq from compute-dtype resolution
gptq gptq from compute-dtype resolution
compressed-tensors compressed-tensors from compute-dtype resolution
fp8 fp8 from compute-dtype resolution
autoround REJECT unsupported-quant-for-derived:autoround
float16 (pure dtype, no quant) omitted float16
bfloat16 (pure dtype, no quant) omitted bfloat16
anything else REJECT unsupported-quant-for-derived — never guess a --quantization

autoround is rejected because the curated AutoRound composes explicitly pass --quantization auto_round; "omit" is not demonstrably safe for an arbitrary derived AutoRound repo and could silently mis-load. It is deferred honestly rather than confidently-wrong.

Compute-dtype resolution for the quantized rows (_resolve_compute_dtype, one defined order — not implementer's choice): (1) der.profile["torch_dtype"] (an additive E1 deriver surface — the deriver already fetched config.json, so this is an additive field, not a frozen-module rewrite) if it normalizes to a real vLLM compute dtype; else (2) the deriver's existing bounded safetensors header probe (deriver.probe_safetensors_dtype, never reimplemented), then normalize: accept only a real compute dtype (float16/half, bfloat16, float32). The probe returns a storage dtype, and quantized safetensors expose int/storage dtypes (I8/U8/I4/…) which are invalid as a vLLM --dtype — any such value is treated as "no usable compute dtype"; else (3) fail closed unsupported-quant-for-derived:missing-torch-dtype. Never default-guess or pass a raw storage dtype. Pure-float16/bfloat16 rows are unaffected — their dtype is the weight_format. The live header-probe fetcher is wired by E2's downloader.set_probe_fetcher() (E4 calls it inside run_pull()); when no fetcher/inputs are present resolution fail-closes at step (3) rather than guess.

The emitted derived compose is patchless default-vLLM: it carries zero Genesis/curated patch insertions, a single absolute-host-path :ro volume, the pinned engine image, and NVIDIA_VISIBLE_DEVICES pinned to the priced GPUs.


5. The download contract (CONTRACT-3)

downloader.download_model(einput, *, fetcher=None) -> DownloadResult. One shared allowlist, used identically by [C2a] sizing, the E2 download, and E3 smoke — there is no parallel list that can drift:

download_set(api) = select_weight_files(api)            # *.safetensors (adapters excluded)
                  + the *.safetensors.index.json (if present, top-level)
                  + REQUIRED_METADATA siblings that exist # config/generation_config/
                                                          # tokenizer*/special_tokens_map/
                                                          # vocab.json/merges.txt
                  + every top-level *.jinja               # chat templates

deriver.sized_download_gb(api) sizes exactly download_set(api), so the [C2a] disk pre-gate prices precisely the set E2 fetches; a test asserts fetched-set == sized-set. The fetcher is huggingface_hub (snapshot_download(repo_id, local_dir, allow_patterns=download_set)) — the established tool, not a hand-rolled fetcher and not the hash-oriented snapshot cache. allow_patterns is mandatory and is literally the shared download_set list. The real huggingface_hub import is lazy and the fetcher is injectable so the test suite is hermetic (no live multi-GB pull in CI).

SHA verification — no-etag is a HARD fail (the deliberate contrast): every *.safetensors is verified by HF HEAD → x-linked-etag → local SHA256 == etag. This reuses setup.sh's pattern, but setup.sh:434/437 prints SKIP and does not count a failure on a missing etag[E] does the opposite: a missing/empty x-linked-etag is a hard failure:"no-etag" abort. [E] never silently trusts an unverifiable multi-GB weight (the aria2c corruption-incident lesson). Metadata files are presence + size only.

DownloadResult.failure is one of null | "no-etag" | "sha-mismatch" | "gated-401" | "disk". This struct is the §6 capture-point-2 payload shape; E2 only returns it (E3 emits the artifact).

Atomic staging + cleanup: download into <HF_HOME>/club3090/pulls/<slug-sanitized>/.incomplete/; on full success and all SHA verified, atomically move the verified tree onto <HF_HOME>/club3090/pulls/<slug-sanitized>/ (the CONTRACT-2 host --model dir). On any failure — gated-401, no-etag, sha-mismatch, disk — the .incomplete (and any sibling staged temp) tree is deleted: no corrupt or partial residue, ever. scripts/tests/test-pullgate-download.sh carries the no-etag fail-closed golden, the gated-401 mid-download case, and the fetched-set == sized-set assertion.


6. The boot path (CONTRACT-4 boot half)

booter.boot_derived(einput, compose_text, *, runner=None) -> BootResult. A derived compose boots only with the proven discipline:

  • docker compose --project-directory <project_dir> -f <compose> up -d — the same --project-directory rule as [D] (relative-path resolution is project-dir-relative);
  • the weights bind-mounted from the HF_HOME host dir <HF_HOME>/club3090/pulls/<slug-sanitized>/ → container /models/club3090/pulls/<slug-sanitized> :ro — the exact mount generate_from_profile() already wrote into compose_text. This is NOT MODEL_DIR=/mnt/models (that is the curated Path-A convention; a derived model's weights live under HF_HOME per CONTRACT-2/3).

The boot is always torn down (runner.down) in a finally, success or failure — no orphaned container/project state (the Pull-Gate on-rig harness rule). runner is injectable; the default shells real docker compose, tests pass a fixture runner (no Docker/GPU in CI). BootResult.failure carries the raw container-died reason — [E] does not classify it into a §6.1 class (that is [F]'s job). The real on-rig boot is pending on-rig validation (E5).


7. The §6 capture artifacts (CONTRACT-4 capture half)

capture.emit_capture(...) writes, schema v1, JSON, redacted, under:

<repo>/.pull-captures/<slug-sanitized>/<utc-ts>/

Four capture points + a top-level manifest:

File Point Fields
pt1-gate.json pre-download gate verdict schema, point, slug, confidence, raw_verdict, terminal, profile_like, hardware_sm
pt2-download.json download point, ok, files, bytes, sha_verified, failure (the DownloadResult shape)
pt3-boot.json boot point, ok, seconds, failure (raw reason; not classified)
pt4-smoke.json post-boot capability-aware smoke point, smoke_capability_set, results, partial
manifest.json §6.2/§6.3 manifest see below

The manifest carries the §6.2 success-consensus key inputs as first-class fields (not only hashed inside the fingerprint — [F] must reason over them; a hash is opaque): model, quant_label, arch_family, topology_class, engine_pin/engine_version (the resolved CONTRACT-2 image spec), kv_calc_version, selected_ctx, kv_format, smoke_capability_set, and topology_summary_canonical (the deterministic serialization of sorted (gpu_name, vram_mib) tuples, §6.2 verbatim). It also carries the submission_fingerprint (the §6.2 stage-2 hash, for quick correlation) and the §6.3 dedup-key inputs as first-class fields (model_id, arch_family, kv_calc_version, engine_version, topology_class, club3090_commit). failure_class is left null by [E] — the §6.1 classifier is [F]'s job; [E] emits the inputs, [F] classifies/trusts/dedups/consumes.

Capture-point 5 — override-accepted force-capture (capture.emit_override_capture(...)): a separate function, written into the same capture directory, emitted only when einput.is_override_accepted (the §5.3 trigger — the override-accepted [E] path). Its fields: point:"override_capture", predicted_b_breakdown (the full [B] kv-calc GB breakdown that produced the verdict), actual:{boot_peak_mib, gpu_worker_reported_mib} (or null if the boot never reached allocation), predicted_vs_actual_delta_mib (or null), exit_error_summary (or null), and the mandatory literal calibration_signal_not_validated: true — §5.3: a forced low-confidence download is a calibration signal, never recorded as fit-validated. The artifact is emitted even when the boot never allocated (then actual is null and exit_error_summary carries why). emit_capture() itself never writes pt5 (test-pullemit-capture.sh asserts pt14 + manifest only).

Redaction: every artifact is written via _redact_text(), which reuses the report.sh --redact convention (the identical sed expression set, driven by the same USER/hostname/HF-token keys) and hardens it further — any absolute internal mount path (/opt/*, /mnt/*, /data/*) is additionally scrubbed to <PATH> (defence-in-depth, the "don't leak internal paths in public artifacts" stack rule). The capture schema carries only slugs / verdicts / relative filenames; it must never carry an absolute internal host path.


8. The derived capability-smoke floor (CONTRACT-4)

capture.smoke_derived(einput, endpoint, *, client=None) -> SmokeResult. A derived generic profile declares no capabilities, so the conservative floor is:

  • plain-chat ALWAYS + streaming ALWAYS — cheap, and catches the #145 class (a model that boots and answers plain chat while streaming/tools are silently dead).
  • tool-call / reasoning-streaming / structured-output / vision / long-context are smoked only if der's config.json positively declares them (read-only positive signals; absence is never inferred as a capability). A generic dense derived model declares none → floor only.
  • every optional capability not smoked → recorded "unsmoked", and partial = any(result == "unsmoked"). Per §6.2 an anchor with un-smoked capabilities is partial and cannot graduate to Tier-1 for those capabilities.

client is injectable (fixture client in CI; the live server is the on-rig E5 leg, pending). test-pullemit-capture.sh asserts a derived model that declares no tools records tool-call:"unsmoked", partial:true.


9. Explicitly OUT of [E] scope

Out Where it lives
The [F] Loop: §6.1 2-tier failure classifier, §6.2 inbound-trust pipeline, §6.3 dedup, consensus, promotion. [E] emits the §6 artifacts; [F] consumes them. [E] does not classify (failure_class stays null), trust, dedup, or promote. Loop phase
Derived tool-call / chat-template / reasoning / curated-or-Genesis-patch support (the CONTRACT-2 deferral — derived is patchless default-vLLM). a future scoped phase
GGUF / .bin backends, multi-quant auto-pick, whichllm hardware slice. v0.8.1
Curated Path-A behaviour, the §4.1 strata / [C0] / [C1] state sets. frozen[E] is strictly post-[C1], consumed not modified
UX doc tracks (§7). post-Pull-Gate

10. Running the executable specs

From the repo root:

bash scripts/tests/test-generate-from-profile.sh   # CONTRACT-2 / CONTRACT-5
bash scripts/tests/test-pullgate-download.sh        # CONTRACT-3
bash scripts/tests/test-pullemit-capture.sh         # CONTRACT-4 (pt1-5 + manifest)
bash scripts/tests/test-pull.sh                     # the [E] continuations + g16-g22 trigger matrix
python3 tools/kv-calc.py --calibration              # must stay Overall: 22/22 (100%)

These are the authoritative spec; this document is the explanatory companion. Where they disagree, the test wins.