[multi-lora] 1/n operation backend: explicit training with Tinker compatibility - #2273
Open
yushengsu-thu wants to merge 94 commits into
Open
[multi-lora] 1/n operation backend: explicit training with Tinker compatibility#2273yushengsu-thu wants to merge 94 commits into
yushengsu-thu wants to merge 94 commits into
Conversation
yushengsu-thu
force-pushed
the
tinker-compatible-backend
branch
6 times, most recently
from
August 8, 2026 20:32
f2e60eb to
7346ed0
Compare
yushengsu-thu
marked this pull request as ready for review
August 10, 2026 03:55
yushengsu-thu
requested review from
Shi-Dong,
Zhichenzzz,
fzyzcjy,
guapisolo,
jybsuper,
maocheng23 and
yueming-yuan
as code owners
August 10, 2026 03:55
…ate serialization
Sample gains optional per-token float channels (loss_weights, advantages)
for client-supplied training data: response-aligned like loss_mask, merged
across turns like the OPD lists (zeros over injected observation spans),
carried on the wire as float32 typed_ragged, CP-sliced like
rollout_log_probs. The binary int32 loss_masks stay untouched.
miles/backends/megatron_utils/tinker_backend/checkpoint.py holds the slot
training-state serialization for the tinker-compatible backend: bf16
adapter weights + positional per-child optimizer state (fp32 masters, Adam
moments, step counters), per-rank atomic shards, rank-0 manifest committed
after a barrier, optional manifest ttl_seconds, and named immutable states
at states/{tag}. Loading fences on format, world topology, and LoRA
rank/alpha shape — never on the display name, so a new registration may
restore another run's state (create-from-checkpoint).
Provenance: #2242 data-channel and checkpoint commits, renamed to the
tinker namespace, minus swap-in/out (they belong to the residency layer).
yushengsu-thu
force-pushed
the
tinker-compatible-backend
branch
from
August 10, 2026 04:38
d8ec792 to
a3d8038
Compare
This was referenced Aug 10, 2026
yushengsu-thu
force-pushed
the
tinker-compatible-backend
branch
2 times, most recently
from
August 10, 2026 23:13
f442377 to
f969743
Compare
Fixed residency: a registration binds the lowest free slot for its whole life or queues behind a full pool (bootstrap drains the queue at retirement); there is no eviction, no bind-at-selection, and therefore no reservation transactions — tenancy changes only on the driver-sequenced register/deregister path. Pins mark slots whose state is immovable (dirty-grads: accumulated gradients no checkpoint carries). The run lifecycle is PENDING -> READY -> RETIRING -> CLEANUP -> COMPLETED, where READY comes from the trainer finishing the slot load — never from a weight publish: serving is a separate axis (serving_version stays 0 until save_weights_for_sampler) and record_weight_update no longer promotes. commit_tinker_step advances the per-run step clock, releases the dirty pin, and honors the optional client-set num_step bound; set_step repositions the baseline for state resume. AdapterRunConfig is the client-driven minimum: rank (server ceiling --lora-rank), optional save/num_step/metadata; alpha is server-resolved and never client-settable. Provenance: #2137 slot pool/registry reworked for fixed residency and readiness/serving decoupling; #2242 tinker lifecycle methods.
…tries, strict execution order One registration is strictly serialized: an operation is claimable only when every earlier ordinal has ARRIVED and reached a terminal state, which carries the client's per-model ordering end to end and keeps an optim_step from ever overtaking its forward_backward batches. Arrival may be out of order — the tinker SDK deliberately posts the first chunk of a large forward_backward last — so operations buffer by ordinal (consecutive from 1 per registration) and a gap below the head blocks all claims until it fills. NOTE: this reorder buffer moves to the tinker frontend when one lands. Retries are fingerprinted (sha256 over kind + canonical payload): re-enqueueing a known operation_id with identical content returns the original operation; different content is a conflict error, never silently swallowed. Cancel applies to QUEUED only and the cancelled ordinal still counts for contiguity; retirement fences open operations; terminal results are retained until acked (enqueue backpressure — mapped to HTTP 429 — is the capacity knob, never result eviction). Provenance: #2242 operation ledger + the arrival/fingerprint upgrades from the design review.
…roller and HTTP surface
…t sums, all-rank NaN veto
The gap-timeout sweep only terminalizes never-arrived QUEUED ordinals; an orphaned CLAIMED head (e.g. a restarted rollout executor whose in-memory runtimes vanished after claiming) blocked its registration's queue forever with no timeout, starving the adapter until deregister. The ledger now stamps claimed_at (monotonic) on the QUEUED->CLAIMED transition, and the backend's sweep heartbeat (control claims, operation_view, service_info) terminal-fails over-age CLAIMED operations with a typed server error naming the operation and its age, routed through fail_tinker_batch — the existing idempotent finalizer that fails only still-CLAIMED operations and releases a batch lease in its finally. --tinker-operation-claimed-ttl configures the TTL (default 1800s: generous because legitimate train steps hold CLAIMED for minutes; <= 0 disables). complete_control_operations now skips already-terminal operations so a late completion racing the sweep is ignored instead of crashing the driver.
A transient child claim failure parked the runtime in FAILED forever: the launch pass only targeted IDLE, so the adapter never claimed again and starved until deregister. The runtime now records last_failure and the launch pass flips FAILED back to IDLE once a fixed 5s cooldown elapses, so one bad claim round costs one cooldown instead of the registration.
ray.exceptions.RayTaskError.__str__ reads traceback_str; without it the driver's tolerated-failure logging path blew up inside the test fake instead of exercising the cap logic.
…structure The api_backends regrouping moved megatron_utils/multi_lora under api_backends/, but the function-local import in the distributed weight-push mixin still targeted the old layout. save_weights_for_sampler crashed at runtime on the multi-LoRA publish path while every CPU gate stayed green, because the import only executes inside _send_one_multi_lora_adapter.
Walk the restructured namespaces (api_backends, ray/multi_lora, rollout/multi_lora, and the frontend package where present) and import every module, then AST-resolve every miles.* import site in miles/ and examples/ — module-level and function-local alike — against the source tree. Function-local imports on the publish path never execute under CPU gates, so a rename that strands one is invisible until a GPU run; this makes the whole class fail fast in tests/fast.
The importlib leg of the import-integrity test pulled every miles.* target under update_weight/, and one of them imports mooncake at module level — absent on hosted CPU CI (and the gate venv). A missing non-miles module is an environment gap, not the stale-layout regression this test pins, so only a miles-module ModuleNotFoundError fails now; the static AST leg still verifies every import site unconditionally.
parse_adapter has no production caller anywhere in the stack (its only reference was its own round-trip test); every sibling in identity.py is production-wired. Found by the zombie-CI audit.
…ombiner contract test runs The D12 contract test (test_metrics_contract.py::test_sdk_combiner_merges_our_chunked_metrics) importorskips on the tinker wheel; without the pin the backend CI never exercised it. Matches the frontend branch's existing pin.
…le tests Multi-LoRA requires --tinker-backend at launch and the tinker rollout fn always attaches the batch execution lease, so the else-branch that trusted per-sample stamped slots (and the two tests exercising it: stamped-slot fallback, non-tinker heterogeneous reward normalization) can never run in production. Convert now fails loudly when an adapter-stamped batch arrives without a lease, a new guard test pins that, and the one legacy-channel test that stamped adapters incidentally now uses plain samples as production legacy batches do.
test_rollout_data_conversion.py::test_unaligned_input_is_trimmed_to_multiple already pins the same trim-to-multiple behavior on the same production branch; keeping a second copy in the padding suite adds maintenance cost without coverage.
The abort_all_requests=False behavior was introduced on main (#2589), not by this stack, so its regression test belongs in a standalone test-only PR against main rather than riding the tinker backend; the file returns to its main-tree content.
_thread_main records a dead loop in run.error and the harness previously still exited 0 after printing the summary, so a wrapper (or a human checking $?) would read an aborted run as a pass; the summary and CSVs still land first, then the process fails if any loop aborted.
yushengsu-thu
commented
Aug 23, 2026
All 35 sites flagged by review 5003387723 on #2273: each multi-line comment, docstring, assert/error message, or argparse help string is now a single line keeping the load-bearing invariant; no behavior change (test-matched substrings preserved).
Collaborator
Author
|
@claude review |
This was referenced Aug 23, 2026
Unacked terminal results of a retired registration lived in the ledger forever (probe: 50 dead registrations retained 14.1 MB, monotonic). fence() now strips request payloads (fingerprints keep retry identity, results stay pollable), and evicting a COMPLETED record from the registry ring fires drop_tenant, purging the tenant's queue and by_id entries. The eviction slice also clamps at zero so under-cap rings no longer evict early. A dropped operation polls as None; the frontend already maps missing operations to typed tombstones.
…uted sync LoRA sync sends only adapter tensors and never refills base weights; opening the session anyway makes begin/end_weight_update restore and re-pack the quantized base buffers with nothing loaded in between, corrupting the frozen base (reproduced on Kimi-K2.5 W4A16, TP8). Also re-adds the update_weight_version abort_all_requests=False wire pin so main #2589's no-abort behavior cannot silently regress. Absorbed from closed PRs #2715 and #2713.
Move the import-integrity checks out of the fast suite and into tests/ci/verify_source_resolution.py, which every CPU and GPU CI job runs before pytest: statically resolve every miles-internal import site (including function-local ones) across miles/ and examples/, walk the optional namespaces in full when present, and import the update_weight lazy-import targets. Failures raise RuntimeError with the offending file:line and import target.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the old dataset-driven Multi-LoRA path with a service-only, client-driven operation backend. Multiple clients share one Megatron base model through fixed LoRA slots and explicitly control forward/backward, optimizer, checkpoint, and sampler-publication boundaries.
This PR provides the backend and Ray-side operation contract. Stacked PR #2346 adds the official
tinker==0.24.1JSON/REST frontend, and #2365 adds the complete client-owned SFT/RL guide.Reviewer
MultiLoraParameterExecutoris deliberately narrower than the whole backend: it owns optimizer step/discard for lease-bound adapter slots. Data conversion and forward/backward live inMultiLoraOperationBatchFn; checkpoint and publication remain Multi-LoRA trainer controls.create_rollout_components()also exposes separateInferenceControllerPortandRolloutExecutorPortroles. Today they are two adapters over the same combinedRolloutManager; the physical controller/executor split remains future integration work.Operation semantics
Supported operations are:
forwardforward_backwardoptim_stepsave_weights_for_samplersave_stateload_stateImportant invariants:
(name, registration_id)prevents stale handles from targeting a re-registered adapter with the same display name.forward_backwardcalls accumulate one client-owned gradient window. A failed batch poisons that window, and the nextoptim_stepdiscards it instead of applying partial gradients.Sampling, scoring, rewards, advantages, batch scheduling, and SDK
Datumconstruction remain client-owned.
Full-parameter seam
The reusable seam is intentionally narrower than the complete backend:
BatchExecutionLeaseTrainerResidencyPortParameterExecutorrun_optim_controlsFullParameterExecutorimplements singleton whole-model Adam step/discardbehavior against a stock Megatron optimizer. It is not connected to launch
configuration, registration, data conversion, forward/backward execution,
checkpointing, controller routing, or sampler publication. This PR therefore
does not claim working full-parameter SFT/RL or full-parameter GPU/E2E support.
Validation
Current head
7189b1e54has:pending checks.
multi-adapter training/checkpointing, sampler publication, restore,
registration fencing, and slot reuse.
Limits
cross_entropy,importance_sampling, orppo.qkv_formatmust bethd.optimizer ownership.
guaranteed; executor-side reconciliation remains future work.
Stack and dependencies
tinker==0.24.1JSON/REST frontend.miles-main.support; merged into the tracked
bridgebranch.0821 liveness fixes: capped tolerance for consecutive generate failures (
--multi-lora-max-consecutive-generate-failures), a claimed-operation TTL backstop that unblocks orphaned CLAIMED queue heads (--tinker-operation-claimed-ttl), and FAILED child runtimes self-healing to IDLE after a cooldown.0821: fixed the publish-path function-local import stranded by the api_backends regrouping (
update_weight .../mixin.py→api_backends.multi_lora.model) +tests/fast/test_import_integrity.pystatic import-integrity regression; 2-GPU mini-loop re-verified publish+sample (PASS).0822 test cleanup: launch-unreachable multi-LoRA tests removed (stamped-slot fallback deleted from production, now a loud ValueError), duplicate trim test dropped, sglang abort regression moved to its own PR [multi-lora] 1/n - 7, test: pin update_weight_version abort_all_requests=False regression #2713, RL-quality harness now exits non-zero on aborted loops.