world-model-optimizer 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
"""Deterministic in-memory fakes of the Tinker SDK surface the distill loop uses.
|
|
2
|
+
|
|
3
|
+
These fakes mirror the shapes of the real SDK (tinker.ServiceClient,
|
|
4
|
+
tinker.TrainingClient, tinker.SamplingClient, tinker.types.Datum and
|
|
5
|
+
tinker.types.SampledSequence) structurally, without importing tinker at all,
|
|
6
|
+
so the test suite runs without the `distill` extra installed. Method names and
|
|
7
|
+
call shapes match the real clients closely enough that the distill loop can be
|
|
8
|
+
exercised end to end against them; the notable simplifications are documented
|
|
9
|
+
on each method (token ids are plain `list[int]` rather than ModelInput chunks,
|
|
10
|
+
results are returned directly rather than through futures).
|
|
11
|
+
|
|
12
|
+
Everything is deterministic: sampled tokens and logprobs are derived from
|
|
13
|
+
SHA-256 hashes of (seed, prompt ids, position), never from time or global
|
|
14
|
+
randomness, so a run replayed with the same inputs produces identical outputs.
|
|
15
|
+
|
|
16
|
+
The fakes also mirror the live service's model-initialization ordering rule:
|
|
17
|
+
tinker accepts LoadWeights only on an uninitialized model, so
|
|
18
|
+
`FakeTrainingClient.load_state` raises once any weight-affecting call has run
|
|
19
|
+
on that client (see `MODEL_INITIALIZING_CALLS`), and every client keeps an
|
|
20
|
+
ordered `calls` log so tests can assert what ran before what. Saved states
|
|
21
|
+
therefore live on the `FakeServiceClient`, not the client that wrote them,
|
|
22
|
+
exactly as real tinker:// state paths outlive their session: a later session's
|
|
23
|
+
freshly created training client can restore them.
|
|
24
|
+
|
|
25
|
+
The fakes also enforce the tokens-in-tokens-out (TITO) invariant that on-policy
|
|
26
|
+
distillation depends on: every sampled span trained on must be byte-identical
|
|
27
|
+
to a span some sampling client actually issued. The issuer set is the training
|
|
28
|
+
client's own linked samplers (its refreshed student weights) plus every
|
|
29
|
+
sampling client its owning FakeServiceClient created (the teacher client the
|
|
30
|
+
warmup phase trains on is created through the service, not linked to the
|
|
31
|
+
student); fabricated or corrupted token ids were issued by nobody and still
|
|
32
|
+
fail. FakeTrainingClient raises AssertionError from forward_backward when a
|
|
33
|
+
datum violates it. Datums flagged `topk=True` (topk-CE replicas) get the
|
|
34
|
+
input-side variant of the check: their TARGETS are intentionally
|
|
35
|
+
teacher-proposed candidate tokens no sampler issued, so the invariant binds
|
|
36
|
+
the model INPUT under the loss-weighted positions instead, which must still
|
|
37
|
+
be the student's exact sampled tokens (see FakeTrainingClient.forward_backward).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import hashlib
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from typing import Literal
|
|
45
|
+
|
|
46
|
+
_SAMPLED_TOKEN_BASE = 32
|
|
47
|
+
_SAMPLED_TOKEN_RANGE = 95
|
|
48
|
+
"""Sampled token ids stay in the printable ASCII range so decode() is total."""
|
|
49
|
+
|
|
50
|
+
MODEL_INITIALIZING_CALLS = frozenset(
|
|
51
|
+
{"load_state", "save_state", "save_weights_for_sampler", "forward_backward", "optim_step"}
|
|
52
|
+
)
|
|
53
|
+
"""Training-client calls after which the live service refuses LoadWeights.
|
|
54
|
+
|
|
55
|
+
Tinker's error is "LoadWeights can only be called on uninitialized models
|
|
56
|
+
(before any weights have been loaded or training has started)", so
|
|
57
|
+
`FakeTrainingClient.load_state` refuses once any of these ran on the same
|
|
58
|
+
client. `get_tokenizer` is deliberately absent: the SDK serves it from a
|
|
59
|
+
metadata-only GetInfo request that never touches weights, so it is safe
|
|
60
|
+
before a restore.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _digest(*parts: str) -> bytes:
|
|
65
|
+
"""A 32-byte SHA-256 digest of the given parts joined unambiguously."""
|
|
66
|
+
return hashlib.sha256("\x1f".join(parts).encode("utf-8")).digest()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ids_key(token_ids: list[int]) -> str:
|
|
70
|
+
return ",".join(str(t) for t in token_ids)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _contains_run(haystack: tuple[int, ...], needle: tuple[int, ...]) -> bool:
|
|
74
|
+
"""Whether `needle` appears as a contiguous run inside `haystack`."""
|
|
75
|
+
if not needle:
|
|
76
|
+
return True
|
|
77
|
+
span = len(needle)
|
|
78
|
+
return any(
|
|
79
|
+
haystack[start : start + span] == needle for start in range(len(haystack) - span + 1)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _derived_token(seed: str, prompt_ids: list[int], sample_index: int, position: int) -> int:
|
|
84
|
+
digest = _digest("token", seed, _ids_key(prompt_ids), str(sample_index), str(position))
|
|
85
|
+
value = int.from_bytes(digest[:4], "big")
|
|
86
|
+
return _SAMPLED_TOKEN_BASE + (value % _SAMPLED_TOKEN_RANGE)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _derived_logprob(*parts: str) -> float:
|
|
90
|
+
"""A deterministic pseudo-logprob in [-4.05, -0.05), from the given parts."""
|
|
91
|
+
digest = _digest("logprob", *parts)
|
|
92
|
+
value = int.from_bytes(digest[:4], "big")
|
|
93
|
+
return -0.05 - (value % 4000) / 1000.0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class FakeTokenizer:
|
|
97
|
+
"""A tiny deterministic char-level tokenizer: token id = code point."""
|
|
98
|
+
|
|
99
|
+
def encode(self, text: str) -> list[int]:
|
|
100
|
+
"""Encode text to token ids (one token per character)."""
|
|
101
|
+
return [ord(ch) for ch in text]
|
|
102
|
+
|
|
103
|
+
def decode(self, token_ids: list[int]) -> str:
|
|
104
|
+
"""Decode token ids back to text."""
|
|
105
|
+
return "".join(chr(t) for t in token_ids)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class FakeSampledSequence:
|
|
110
|
+
"""One sampled sequence, mirroring tinker.types.SampledSequence.
|
|
111
|
+
|
|
112
|
+
`tokens` are the generated token ids and `logprobs[j]` is the logprob the
|
|
113
|
+
sampler assigned to `tokens[j]`, aligned one to one.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
tokens: list[int]
|
|
117
|
+
logprobs: list[float]
|
|
118
|
+
stop_reason: Literal["length", "stop"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class FakeForwardBackwardOutput:
|
|
123
|
+
"""One forward/backward result, mirroring tinker.types.ForwardBackwardOutput.
|
|
124
|
+
|
|
125
|
+
The real output carries per-datum `loss_fn_outputs` tensors (the cookbook
|
|
126
|
+
reads a per-datum "logprobs" TensorData) plus a server-populated
|
|
127
|
+
`metrics: dict[str, float]` whose keys carry a ":reduction" suffix for
|
|
128
|
+
the SDK's chunk combiner. The fake keeps the per-datum shape (one empty
|
|
129
|
+
dict per datum) and reports a deterministic batch loss under
|
|
130
|
+
"total_loss:sum" (the cookbook's "total_loss" name plus the combiner
|
|
131
|
+
suffix), so adapters exercise the same suffix-tolerant metric extraction
|
|
132
|
+
they run against the real SDK.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
loss_fn_output_type: str
|
|
136
|
+
loss_fn_outputs: list[dict[str, list[float]]]
|
|
137
|
+
metrics: dict[str, float]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class FakeOptimStepResponse:
|
|
142
|
+
"""One optimizer-step result, mirroring tinker.types.OptimStepResponse.
|
|
143
|
+
|
|
144
|
+
The real response carries only an untyped optional metrics mapping; the
|
|
145
|
+
fake reports a deterministic "grad_norm:mean" so adapters can prove their
|
|
146
|
+
extraction plumbing on a stable value.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
metrics: dict[str, float] | None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class IssuedSample:
|
|
154
|
+
"""A record of one span a sampling client issued: the TITO ground truth."""
|
|
155
|
+
|
|
156
|
+
prompt_ids: tuple[int, ...]
|
|
157
|
+
sampled_ids: tuple[int, ...]
|
|
158
|
+
logprobs: tuple[float, ...]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class _SpanLedger:
|
|
163
|
+
"""Issued-span records shared by a training client and its samplers."""
|
|
164
|
+
|
|
165
|
+
records: list[IssuedSample] = field(default_factory=list)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class FakeDatum:
|
|
169
|
+
"""A training datum, mirroring tinker.types.Datum structurally.
|
|
170
|
+
|
|
171
|
+
The real Datum carries a ModelInput plus tensor-valued loss_fn_inputs;
|
|
172
|
+
here everything is plain lists. `model_input_tokens` is the full input
|
|
173
|
+
sequence and the loss inputs are aligned with `target_tokens`.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
model_input_tokens: The full input token sequence (all but the final
|
|
177
|
+
target position, in the usual shifted-by-one layout; the fakes do
|
|
178
|
+
not enforce that layout).
|
|
179
|
+
target_tokens: Tokens the loss is computed over.
|
|
180
|
+
weights: Per-target-token loss weights; positions with weight 0 are
|
|
181
|
+
prompt/tool tokens outside the sampled spans. Defaults to all 1.0.
|
|
182
|
+
advantages: Optional per-target-token advantages (importance_sampling).
|
|
183
|
+
logprobs: Optional per-target-token behavior-policy logprobs.
|
|
184
|
+
topk: Marks a topk-CE replica: its targets are teacher-proposed
|
|
185
|
+
candidates (fractional weights), so the TITO check binds the
|
|
186
|
+
model INPUT under the weighted positions instead of the targets.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
model_input_tokens: list[int],
|
|
192
|
+
target_tokens: list[int],
|
|
193
|
+
weights: list[float] | None = None,
|
|
194
|
+
advantages: list[float] | None = None,
|
|
195
|
+
logprobs: list[float] | None = None,
|
|
196
|
+
topk: bool = False,
|
|
197
|
+
) -> None:
|
|
198
|
+
self.model_input_tokens = list(model_input_tokens)
|
|
199
|
+
self.target_tokens = list(target_tokens)
|
|
200
|
+
self.weights = list(weights) if weights is not None else [1.0] * len(target_tokens)
|
|
201
|
+
self.advantages = list(advantages) if advantages is not None else []
|
|
202
|
+
self.logprobs = list(logprobs) if logprobs is not None else []
|
|
203
|
+
self.topk = topk
|
|
204
|
+
if len(self.weights) != len(self.target_tokens):
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"weights length {len(self.weights)} does not match "
|
|
207
|
+
f"target_tokens length {len(self.target_tokens)}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def sampled_spans(self) -> list[tuple[int, ...]]:
|
|
211
|
+
"""Maximal contiguous runs of target tokens with nonzero weight."""
|
|
212
|
+
spans: list[tuple[int, ...]] = []
|
|
213
|
+
current: list[int] = []
|
|
214
|
+
for token, weight in zip(self.target_tokens, self.weights, strict=True):
|
|
215
|
+
if weight != 0.0:
|
|
216
|
+
current.append(token)
|
|
217
|
+
elif current:
|
|
218
|
+
spans.append(tuple(current))
|
|
219
|
+
current = []
|
|
220
|
+
if current:
|
|
221
|
+
spans.append(tuple(current))
|
|
222
|
+
return spans
|
|
223
|
+
|
|
224
|
+
def input_loss_spans(self) -> list[tuple[int, ...]]:
|
|
225
|
+
"""Model-INPUT token runs under the nonzero-weight target positions.
|
|
226
|
+
|
|
227
|
+
Target index j scores unshifted position j + 1, whose token sits at
|
|
228
|
+
model_input index j + 1 when that index exists (the final target's
|
|
229
|
+
token was shifted out of the input, so a loss run reaching the
|
|
230
|
+
sequence end contributes one token fewer here). This is what the
|
|
231
|
+
TITO check inspects for topk-CE replicas: the targets are candidate
|
|
232
|
+
tokens by design, but the input context at the loss positions must
|
|
233
|
+
still be tokens a sampler actually issued.
|
|
234
|
+
"""
|
|
235
|
+
spans: list[tuple[int, ...]] = []
|
|
236
|
+
current: list[int] = []
|
|
237
|
+
for index, weight in enumerate(self.weights):
|
|
238
|
+
in_input = index + 1 < len(self.model_input_tokens)
|
|
239
|
+
if weight != 0.0 and in_input:
|
|
240
|
+
current.append(self.model_input_tokens[index + 1])
|
|
241
|
+
continue
|
|
242
|
+
if current:
|
|
243
|
+
spans.append(tuple(current))
|
|
244
|
+
current = []
|
|
245
|
+
if current:
|
|
246
|
+
spans.append(tuple(current))
|
|
247
|
+
return spans
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class FakeSamplingClient:
|
|
251
|
+
"""Deterministic stand-in for tinker.SamplingClient.
|
|
252
|
+
|
|
253
|
+
Simplifications vs the real client: prompts are `list[int]` (the real
|
|
254
|
+
client takes ModelInput), results are returned directly (no futures), and
|
|
255
|
+
one call returns one sequence.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
seed: Seed string, by convention the fake sampler weights path; all
|
|
259
|
+
sampled tokens and logprobs derive from it.
|
|
260
|
+
ledger: Shared issued-span ledger; samplers refreshed from the same
|
|
261
|
+
training client share one ledger so TITO checks see all of them.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def __init__(self, seed: str, ledger: _SpanLedger | None = None) -> None:
|
|
265
|
+
self.seed = seed
|
|
266
|
+
self._ledger = ledger if ledger is not None else _SpanLedger()
|
|
267
|
+
self.issued: list[IssuedSample] = []
|
|
268
|
+
|
|
269
|
+
def sample(
|
|
270
|
+
self,
|
|
271
|
+
prompt_token_ids: list[int],
|
|
272
|
+
max_tokens: int,
|
|
273
|
+
temperature: float,
|
|
274
|
+
stop: list[int] | list[str] | None = None,
|
|
275
|
+
sample_index: int = 0,
|
|
276
|
+
) -> FakeSampledSequence:
|
|
277
|
+
"""Sample a deterministic sequence for the prompt and record it.
|
|
278
|
+
|
|
279
|
+
Tokens and logprobs derive purely from a hash of (seed, prompt ids,
|
|
280
|
+
sample_index, position): calling again with identical arguments
|
|
281
|
+
returns an identical sequence. Pass distinct `sample_index` values to
|
|
282
|
+
get distinct group members deterministically (the real SDK's
|
|
283
|
+
num_samples analogue). `temperature` is accepted for signature parity
|
|
284
|
+
and does not affect the fake's output.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
prompt_token_ids: Prompt tokens the sample conditions on.
|
|
288
|
+
max_tokens: Maximum number of tokens to generate.
|
|
289
|
+
temperature: Accepted for parity; unused by the fake.
|
|
290
|
+
stop: Optional stop token ids or stop strings (the real
|
|
291
|
+
SamplingParams accepts both). A generated stop token is
|
|
292
|
+
included in the output and ends generation with stop_reason
|
|
293
|
+
"stop"; a stop string fires when the generation so far, decoded
|
|
294
|
+
with the char-level FakeTokenizer convention, ends with it.
|
|
295
|
+
sample_index: Deterministic nonce distinguishing group members.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
The sampled sequence with aligned per-token logprobs.
|
|
299
|
+
"""
|
|
300
|
+
del temperature
|
|
301
|
+
int_stops = {item for item in (stop or ()) if isinstance(item, int)}
|
|
302
|
+
str_stops = [item for item in (stop or ()) if isinstance(item, str)]
|
|
303
|
+
tokens: list[int] = []
|
|
304
|
+
logprobs: list[float] = []
|
|
305
|
+
stop_reason: Literal["length", "stop"] = "length"
|
|
306
|
+
for position in range(max_tokens):
|
|
307
|
+
token = _derived_token(self.seed, prompt_token_ids, sample_index, position)
|
|
308
|
+
logprob = _derived_logprob(
|
|
309
|
+
"issued", self.seed, _ids_key(prompt_token_ids), str(sample_index), str(position)
|
|
310
|
+
)
|
|
311
|
+
tokens.append(token)
|
|
312
|
+
logprobs.append(logprob)
|
|
313
|
+
if token in int_stops:
|
|
314
|
+
stop_reason = "stop"
|
|
315
|
+
break
|
|
316
|
+
if str_stops:
|
|
317
|
+
text = "".join(chr(t) for t in tokens)
|
|
318
|
+
if any(text.endswith(item) for item in str_stops):
|
|
319
|
+
stop_reason = "stop"
|
|
320
|
+
break
|
|
321
|
+
record = IssuedSample(
|
|
322
|
+
prompt_ids=tuple(prompt_token_ids),
|
|
323
|
+
sampled_ids=tuple(tokens),
|
|
324
|
+
logprobs=tuple(logprobs),
|
|
325
|
+
)
|
|
326
|
+
self.issued.append(record)
|
|
327
|
+
self._ledger.records.append(record)
|
|
328
|
+
return FakeSampledSequence(tokens=tokens, logprobs=logprobs, stop_reason=stop_reason)
|
|
329
|
+
|
|
330
|
+
def compute_logprobs(self, token_ids: list[int]) -> list[float | None]:
|
|
331
|
+
"""Per-position logprobs for a full token sequence.
|
|
332
|
+
|
|
333
|
+
Indexing convention (mirroring the real SDK's prompt_logprobs): index
|
|
334
|
+
i holds the logprob of token i given tokens < i, and index 0 is None
|
|
335
|
+
because the first token has no context (the real SDK likewise returns
|
|
336
|
+
None where a logprob cannot be computed; we chose None over a
|
|
337
|
+
placeholder to match it exactly).
|
|
338
|
+
|
|
339
|
+
Positions covered by a previously issued sampled span, meaning the
|
|
340
|
+
span's sampled ids appear in `token_ids` immediately after that span's
|
|
341
|
+
exact prompt ids, echo the exact logprobs issued at sampling time
|
|
342
|
+
(first matching record wins for overlaps). Every other position gets
|
|
343
|
+
a deterministic hash-derived value from (seed, tokens <= i).
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
token_ids: The full sequence to score.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
One entry per input position; entry 0 is None.
|
|
350
|
+
"""
|
|
351
|
+
n = len(token_ids)
|
|
352
|
+
result: list[float | None] = [None] * n
|
|
353
|
+
filled = [False] * n
|
|
354
|
+
if n > 0:
|
|
355
|
+
filled[0] = True # position 0 stays None by convention
|
|
356
|
+
for record in self._ledger.records:
|
|
357
|
+
span_len = len(record.sampled_ids)
|
|
358
|
+
prompt_len = len(record.prompt_ids)
|
|
359
|
+
if span_len == 0:
|
|
360
|
+
continue
|
|
361
|
+
for start in range(prompt_len, n - span_len + 1):
|
|
362
|
+
if tuple(token_ids[start : start + span_len]) != record.sampled_ids:
|
|
363
|
+
continue
|
|
364
|
+
if tuple(token_ids[start - prompt_len : start]) != record.prompt_ids:
|
|
365
|
+
continue
|
|
366
|
+
for offset in range(span_len):
|
|
367
|
+
position = start + offset
|
|
368
|
+
if not filled[position]:
|
|
369
|
+
result[position] = record.logprobs[offset]
|
|
370
|
+
filled[position] = True
|
|
371
|
+
for position in range(1, n):
|
|
372
|
+
if not filled[position]:
|
|
373
|
+
result[position] = _derived_logprob(
|
|
374
|
+
"context", self.seed, _ids_key(token_ids[: position + 1])
|
|
375
|
+
)
|
|
376
|
+
return result
|
|
377
|
+
|
|
378
|
+
def topk_prompt_logprobs(
|
|
379
|
+
self, token_ids: list[int], k: int
|
|
380
|
+
) -> tuple[list[float | None], list[list[tuple[int, float]] | None]]:
|
|
381
|
+
"""Deterministic echo of the SDK's prefill-only top-k prompt logprobs.
|
|
382
|
+
|
|
383
|
+
Mirrors `sample(prompt=token_ids, max_tokens=1,
|
|
384
|
+
include_prompt_logprobs=True, topk_prompt_logprobs=k)` on the real
|
|
385
|
+
client: the first return value is the realized per-position logprobs
|
|
386
|
+
(exactly `compute_logprobs(token_ids)`, so issued spans echo their
|
|
387
|
+
sampling-time logprobs), and the second is one top-k candidate list
|
|
388
|
+
per position (None at position 0, which has no context). At each
|
|
389
|
+
scoreable position the top-1 candidate is the sequence's own token
|
|
390
|
+
with its realized logprob; the remaining k - 1 candidates carry
|
|
391
|
+
hash-derived token ids and strictly decreasing hash-derived logprobs
|
|
392
|
+
below it, so ranks are unambiguous and the whole result is a pure
|
|
393
|
+
function of (seed, ledger echoes, token_ids, k): replaying the same
|
|
394
|
+
call always returns the identical value.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
token_ids: The full sequence to score, prompt-style.
|
|
398
|
+
k: Candidates per position (>= 1).
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
The (realized logprobs, top-k rows) pair, both with one entry
|
|
402
|
+
per input position.
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If `k` is not positive.
|
|
406
|
+
"""
|
|
407
|
+
if k < 1:
|
|
408
|
+
raise ValueError(f"k must be >= 1, got {k}")
|
|
409
|
+
if k > _SAMPLED_TOKEN_RANGE:
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"the fake vocabulary has only {_SAMPLED_TOKEN_RANGE} derivable "
|
|
412
|
+
f"candidate ids per position, got k = {k} (the config caps "
|
|
413
|
+
"train.topk at 64, well inside it)"
|
|
414
|
+
)
|
|
415
|
+
realized = self.compute_logprobs(token_ids)
|
|
416
|
+
rows: list[list[tuple[int, float]] | None] = [None] * len(token_ids)
|
|
417
|
+
for position in range(1, len(token_ids)):
|
|
418
|
+
top_logprob = realized[position]
|
|
419
|
+
assert top_logprob is not None # compute_logprobs fills every p >= 1
|
|
420
|
+
entries: list[tuple[int, float]] = [(token_ids[position], top_logprob)]
|
|
421
|
+
seen = {token_ids[position]}
|
|
422
|
+
logprob = top_logprob
|
|
423
|
+
rank = 1
|
|
424
|
+
while len(entries) < k:
|
|
425
|
+
token = _derived_token(f"topk:{self.seed}", token_ids[:position], rank, position)
|
|
426
|
+
while token in seen:
|
|
427
|
+
token = _SAMPLED_TOKEN_BASE + (
|
|
428
|
+
(token - _SAMPLED_TOKEN_BASE + 1) % _SAMPLED_TOKEN_RANGE
|
|
429
|
+
)
|
|
430
|
+
seen.add(token)
|
|
431
|
+
logprob += _derived_logprob(
|
|
432
|
+
"topk-gap", self.seed, _ids_key(token_ids[: position + 1]), str(rank)
|
|
433
|
+
)
|
|
434
|
+
entries.append((token, logprob))
|
|
435
|
+
rank += 1
|
|
436
|
+
rows[position] = entries
|
|
437
|
+
return realized, rows
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class FakeTrainingClient:
|
|
441
|
+
"""Deterministic stand-in for tinker.TrainingClient.
|
|
442
|
+
|
|
443
|
+
Create via FakeServiceClient.create_lora_training_client. Simplifications
|
|
444
|
+
vs the real client: results return directly (no APIFuture), datums are
|
|
445
|
+
FakeDatum (plain lists rather than tensors), and optim_step takes a bare
|
|
446
|
+
learning rate rather than AdamParams.
|
|
447
|
+
|
|
448
|
+
Like the real client this one is UNINITIALIZED until a weight-affecting
|
|
449
|
+
call runs on it, and `load_state` is legal only while it stays that way
|
|
450
|
+
(see `MODEL_INITIALIZING_CALLS` and `load_state`).
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
def __init__(self, service: FakeServiceClient, base_model: str, rank: int) -> None:
|
|
454
|
+
self.base_model = base_model
|
|
455
|
+
self.rank = rank
|
|
456
|
+
self.step_count = 0
|
|
457
|
+
self.forward_backward_calls: list[tuple[list[FakeDatum], str]] = []
|
|
458
|
+
self.optim_step_lrs: list[float] = []
|
|
459
|
+
self.calls: list[str] = []
|
|
460
|
+
"""Ordered log of every method called on this client, oldest first."""
|
|
461
|
+
|
|
462
|
+
self._service = service
|
|
463
|
+
self._ledger = _SpanLedger()
|
|
464
|
+
self._sampler_counter = 0
|
|
465
|
+
|
|
466
|
+
def get_tokenizer(self) -> FakeTokenizer:
|
|
467
|
+
"""The deterministic char-level tokenizer for this fake model.
|
|
468
|
+
|
|
469
|
+
Logged like every other call, but absent from
|
|
470
|
+
`MODEL_INITIALIZING_CALLS`: the real SDK answers it from a
|
|
471
|
+
metadata-only GetInfo request, so it never blocks a later restore.
|
|
472
|
+
"""
|
|
473
|
+
self.calls.append("get_tokenizer")
|
|
474
|
+
return FakeTokenizer()
|
|
475
|
+
|
|
476
|
+
def forward_backward(self, datums: list[FakeDatum], loss_fn: str) -> FakeForwardBackwardOutput:
|
|
477
|
+
"""Record a training batch after asserting the TITO invariant.
|
|
478
|
+
|
|
479
|
+
Every sampled span in every datum (maximal nonzero-weight run of
|
|
480
|
+
target tokens) must exactly equal the sampled ids of some span an
|
|
481
|
+
eligible issuer previously issued: a linked sampling client (this
|
|
482
|
+
training client's refreshed student weights) or any sampling client
|
|
483
|
+
the owning FakeServiceClient created (the teacher client the warmup
|
|
484
|
+
phase trains on). Fabricated ids fail either way.
|
|
485
|
+
|
|
486
|
+
Datums flagged `topk=True` (topk-CE replicas) are checked on the
|
|
487
|
+
model INPUT instead of the targets: their targets are intentionally
|
|
488
|
+
teacher-proposed candidate tokens that no sampler ever issued (that
|
|
489
|
+
is the whole point of the loss), while the input context must remain
|
|
490
|
+
the student's exact sampled tokens. Each input-side loss span
|
|
491
|
+
(`FakeDatum.input_loss_spans`) must appear as a contiguous run inside
|
|
492
|
+
some issued span (a contiguous run rather than the whole span, since
|
|
493
|
+
the next-token shift truncates a sequence-final span by one token and
|
|
494
|
+
rank padding can split a run). Topk replicas are additionally pinned
|
|
495
|
+
to the cross_entropy loss.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
datums: The batch to train on.
|
|
499
|
+
loss_fn: Loss function name (e.g. "importance_sampling", "ppo",
|
|
500
|
+
or "cross_entropy"); recorded but not interpreted beyond the
|
|
501
|
+
topk-replica pin above.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
A deterministic SDK-shaped output: the metrics dict carries a
|
|
505
|
+
"total_loss:sum" value derived purely from (loss_fn, batch
|
|
506
|
+
target tokens), so the same batch always reports the same loss.
|
|
507
|
+
|
|
508
|
+
Raises:
|
|
509
|
+
AssertionError: If a sampled span was never issued by an eligible
|
|
510
|
+
issuer (the message names the datum, the span, and the first
|
|
511
|
+
mismatching token position against the closest issued span),
|
|
512
|
+
if a topk replica's input-side loss span appears in no issued
|
|
513
|
+
span, or if a topk replica arrives under a loss other than
|
|
514
|
+
cross_entropy.
|
|
515
|
+
"""
|
|
516
|
+
records = self._issuer_records()
|
|
517
|
+
issued = {record.sampled_ids for record in records}
|
|
518
|
+
for datum_index, datum in enumerate(datums):
|
|
519
|
+
if datum.topk:
|
|
520
|
+
if loss_fn != "cross_entropy":
|
|
521
|
+
raise AssertionError(
|
|
522
|
+
f"topk-CE replica datum {datum_index} was trained under "
|
|
523
|
+
f"loss_fn {loss_fn!r}; candidate targets are only valid "
|
|
524
|
+
"under cross_entropy"
|
|
525
|
+
)
|
|
526
|
+
for span in datum.input_loss_spans():
|
|
527
|
+
if any(_contains_run(record.sampled_ids, span) for record in records):
|
|
528
|
+
continue
|
|
529
|
+
raise AssertionError(
|
|
530
|
+
f"TITO violation in topk datum {datum_index}: the model input "
|
|
531
|
+
f"under a loss-weighted span (length {len(span)}) matches no "
|
|
532
|
+
"issued span; topk-CE may propose candidate TARGETS, but the "
|
|
533
|
+
"input context must stay the student's exact sampled tokens"
|
|
534
|
+
)
|
|
535
|
+
continue
|
|
536
|
+
for span in datum.sampled_spans():
|
|
537
|
+
if span in issued:
|
|
538
|
+
continue
|
|
539
|
+
raise AssertionError(self._tito_message(datum_index, span, records))
|
|
540
|
+
self.calls.append("forward_backward")
|
|
541
|
+
self.forward_backward_calls.append((list(datums), loss_fn))
|
|
542
|
+
loss = -_derived_logprob(
|
|
543
|
+
"batch-loss", loss_fn, *(_ids_key(datum.target_tokens) for datum in datums)
|
|
544
|
+
)
|
|
545
|
+
return FakeForwardBackwardOutput(
|
|
546
|
+
loss_fn_output_type="FakeLossReturn",
|
|
547
|
+
loss_fn_outputs=[{} for _ in datums],
|
|
548
|
+
metrics={"total_loss:sum": loss},
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def _issuer_records(self) -> list[IssuedSample]:
|
|
552
|
+
"""Every span an eligible issuer recorded: linked ledger + service clients."""
|
|
553
|
+
records = list(self._ledger.records)
|
|
554
|
+
records.extend(self._service.issued_records())
|
|
555
|
+
return records
|
|
556
|
+
|
|
557
|
+
def _tito_message(
|
|
558
|
+
self, datum_index: int, span: tuple[int, ...], records: list[IssuedSample]
|
|
559
|
+
) -> str:
|
|
560
|
+
"""Build the TITO failure message naming the first mismatch position."""
|
|
561
|
+
best: IssuedSample | None = None
|
|
562
|
+
best_prefix = -1
|
|
563
|
+
for record in records:
|
|
564
|
+
prefix = 0
|
|
565
|
+
for a, b in zip(span, record.sampled_ids, strict=False):
|
|
566
|
+
if a != b:
|
|
567
|
+
break
|
|
568
|
+
prefix += 1
|
|
569
|
+
if prefix > best_prefix:
|
|
570
|
+
best_prefix = prefix
|
|
571
|
+
best = record
|
|
572
|
+
if best is None:
|
|
573
|
+
return (
|
|
574
|
+
f"TITO violation in datum {datum_index}: sampled span of length "
|
|
575
|
+
f"{len(span)} trained on, but no eligible sampler issued any span"
|
|
576
|
+
)
|
|
577
|
+
mismatch = best_prefix
|
|
578
|
+
if mismatch < len(span) and mismatch < len(best.sampled_ids):
|
|
579
|
+
detail = (
|
|
580
|
+
f"first mismatch at position {mismatch}: datum has {span[mismatch]}, "
|
|
581
|
+
f"closest issued span has {best.sampled_ids[mismatch]}"
|
|
582
|
+
)
|
|
583
|
+
else:
|
|
584
|
+
detail = (
|
|
585
|
+
f"first mismatch at position {mismatch}: datum span length {len(span)} "
|
|
586
|
+
f"vs closest issued span length {len(best.sampled_ids)}"
|
|
587
|
+
)
|
|
588
|
+
return f"TITO violation in datum {datum_index}: {detail}"
|
|
589
|
+
|
|
590
|
+
def optim_step(self, learning_rate: float) -> FakeOptimStepResponse:
|
|
591
|
+
"""Apply one optimizer step: increments the step counter.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
A deterministic SDK-shaped response whose metrics carry a
|
|
595
|
+
"grad_norm:mean" derived purely from (step count, learning rate).
|
|
596
|
+
"""
|
|
597
|
+
self.calls.append("optim_step")
|
|
598
|
+
self.optim_step_lrs.append(learning_rate)
|
|
599
|
+
self.step_count += 1
|
|
600
|
+
grad_norm = -_derived_logprob("grad-norm", str(self.step_count), str(learning_rate))
|
|
601
|
+
return FakeOptimStepResponse(metrics={"grad_norm:mean": grad_norm})
|
|
602
|
+
|
|
603
|
+
def save_state(self) -> str:
|
|
604
|
+
"""Save training state, returning a fake tinker:// state path.
|
|
605
|
+
|
|
606
|
+
The state lands in the owning FakeServiceClient's artifact store, not
|
|
607
|
+
on this client: real tinker:// state paths outlive the session that
|
|
608
|
+
wrote them, which is what lets a later session restore them on a
|
|
609
|
+
freshly created training client.
|
|
610
|
+
"""
|
|
611
|
+
self.calls.append("save_state")
|
|
612
|
+
return self._service._register_state(self.step_count)
|
|
613
|
+
|
|
614
|
+
def load_state(self, path: str) -> None:
|
|
615
|
+
"""Restore a previously saved state; must be this client's FIRST call.
|
|
616
|
+
|
|
617
|
+
Mirrors the live service's rule that LoadWeights is accepted only on
|
|
618
|
+
an uninitialized model: once any call in `MODEL_INITIALIZING_CALLS`
|
|
619
|
+
has run here, restoring is refused. That is the exact failure a
|
|
620
|
+
resumed distillation run hits when anything (a sampler-weights save,
|
|
621
|
+
a forward pass, or an earlier abandoned restore) touches the training
|
|
622
|
+
client before the checkpoint is loaded.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
path: A path returned by save_state on any training client of the
|
|
626
|
+
owning service.
|
|
627
|
+
|
|
628
|
+
Raises:
|
|
629
|
+
RuntimeError: If a weight-affecting call already ran on this
|
|
630
|
+
client; the message names them in order.
|
|
631
|
+
ValueError: If the path was never returned by save_state.
|
|
632
|
+
"""
|
|
633
|
+
initialized = [call for call in self.calls if call in MODEL_INITIALIZING_CALLS]
|
|
634
|
+
if initialized:
|
|
635
|
+
raise RuntimeError(
|
|
636
|
+
"LoadWeights can only be called on uninitialized models: load_state "
|
|
637
|
+
f"came after {initialized} on this training client. Restore the "
|
|
638
|
+
"checkpoint as the first call on a freshly created training client"
|
|
639
|
+
)
|
|
640
|
+
step_count = self._service._saved_state_step(path)
|
|
641
|
+
self.calls.append("load_state")
|
|
642
|
+
self.step_count = step_count
|
|
643
|
+
|
|
644
|
+
def save_weights_for_sampler(self, name: str) -> str:
|
|
645
|
+
"""Save current weights for sampling, returning a fake sampler path.
|
|
646
|
+
|
|
647
|
+
The path can be exchanged for a linked FakeSamplingClient via
|
|
648
|
+
FakeServiceClient.create_sampling_client.
|
|
649
|
+
"""
|
|
650
|
+
self.calls.append("save_weights_for_sampler")
|
|
651
|
+
path = f"tinker://fake/sampler/{name}/{self._sampler_counter}"
|
|
652
|
+
self._sampler_counter += 1
|
|
653
|
+
self._service._register_sampler_path(path, self)
|
|
654
|
+
return path
|
|
655
|
+
|
|
656
|
+
def save_weights_and_get_sampling_client(self, name: str) -> FakeSamplingClient:
|
|
657
|
+
"""Save current weights and return a fresh linked sampling client.
|
|
658
|
+
|
|
659
|
+
Each call yields a distinct sampler path (so a refreshed sampler
|
|
660
|
+
samples different tokens) whose client shares this training client's
|
|
661
|
+
span ledger (so TITO checks see every sampler's issued spans).
|
|
662
|
+
"""
|
|
663
|
+
path = self.save_weights_for_sampler(name)
|
|
664
|
+
return FakeSamplingClient(seed=path, ledger=self._ledger)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class FakeServiceClient:
|
|
668
|
+
"""Deterministic stand-in for tinker.ServiceClient.
|
|
669
|
+
|
|
670
|
+
Every sampling client it creates is remembered as a potential TITO issuer
|
|
671
|
+
(see `issued_records`): the real service serves the teacher and the
|
|
672
|
+
student through the same account, so tokens the teacher client genuinely
|
|
673
|
+
sampled are legitimate training targets for the warmup phase, while ids no
|
|
674
|
+
client ever issued remain violations.
|
|
675
|
+
|
|
676
|
+
The service also owns the saved-state artifact store, so a state one
|
|
677
|
+
training client wrote can be restored by another (what a resumed run
|
|
678
|
+
does): every `create_lora_training_client` call yields a fresh,
|
|
679
|
+
uninitialized client, just like the real service.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
def __init__(self) -> None:
|
|
683
|
+
self._sampler_paths: dict[str, FakeTrainingClient] = {}
|
|
684
|
+
self._sampling_clients: list[FakeSamplingClient] = []
|
|
685
|
+
self._states: dict[str, int] = {}
|
|
686
|
+
self._state_counter = 0
|
|
687
|
+
|
|
688
|
+
def create_lora_training_client(self, base_model: str, rank: int = 32) -> FakeTrainingClient:
|
|
689
|
+
"""Create a fresh, uninitialized fake LoRA training client."""
|
|
690
|
+
return FakeTrainingClient(service=self, base_model=base_model, rank=rank)
|
|
691
|
+
|
|
692
|
+
def create_sampling_client(self, model_path: str) -> FakeSamplingClient:
|
|
693
|
+
"""Create (and track as a TITO issuer) a sampling client for a path.
|
|
694
|
+
|
|
695
|
+
Paths produced by a linked training client's save_weights_for_sampler
|
|
696
|
+
yield clients that share that training client's span ledger; any other
|
|
697
|
+
path (e.g. a base model name for a standalone teacher) yields an
|
|
698
|
+
unlinked client seeded with the path. Either way the client is tracked
|
|
699
|
+
so its issued spans satisfy the training clients' TITO checks.
|
|
700
|
+
"""
|
|
701
|
+
training = self._sampler_paths.get(model_path)
|
|
702
|
+
if training is not None:
|
|
703
|
+
client = FakeSamplingClient(seed=model_path, ledger=training._ledger)
|
|
704
|
+
else:
|
|
705
|
+
client = FakeSamplingClient(seed=model_path)
|
|
706
|
+
self._sampling_clients.append(client)
|
|
707
|
+
return client
|
|
708
|
+
|
|
709
|
+
def issued_records(self) -> list[IssuedSample]:
|
|
710
|
+
"""Every span any sampling client created by this service issued."""
|
|
711
|
+
return [record for client in self._sampling_clients for record in client.issued]
|
|
712
|
+
|
|
713
|
+
def _register_sampler_path(self, path: str, training: FakeTrainingClient) -> None:
|
|
714
|
+
self._sampler_paths[path] = training
|
|
715
|
+
|
|
716
|
+
def _register_state(self, step_count: int) -> str:
|
|
717
|
+
"""Store one saved state, returning its fresh service-wide path."""
|
|
718
|
+
path = f"tinker://fake/state/{self._state_counter}"
|
|
719
|
+
self._state_counter += 1
|
|
720
|
+
self._states[path] = step_count
|
|
721
|
+
return path
|
|
722
|
+
|
|
723
|
+
def _saved_state_step(self, path: str) -> int:
|
|
724
|
+
"""The step count a saved state carries.
|
|
725
|
+
|
|
726
|
+
Raises:
|
|
727
|
+
ValueError: If no training client of this service saved `path`.
|
|
728
|
+
"""
|
|
729
|
+
if path not in self._states:
|
|
730
|
+
raise ValueError(
|
|
731
|
+
f"unknown state path {path!r}: it was never returned by save_state "
|
|
732
|
+
"on a training client of this service"
|
|
733
|
+
)
|
|
734
|
+
return self._states[path]
|