@tuned-tensor/cli 0.20.0 → 0.22.0
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.
- package/README.md +123 -7
- package/dist/{chunk-4Q54NHKR.js → chunk-4SUXBHIY.js} +1715 -392
- package/dist/chunk-4SUXBHIY.js.map +1 -0
- package/dist/index.js +1910 -356
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +147 -49
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +44 -1
- package/docs/local-runtime/aws-gpu.md +2 -0
- package/docs/local-runtime/decision.md +142 -0
- package/docs/spec-architecture.html +2 -2
- package/docs/spec-workflow.md +18 -3
- package/examples/single-spec/decision/tunedtensor.json +630 -0
- package/package.json +5 -2
- package/training/adapter/src/evaluate.py +14 -7
- package/training/adapter/src/hub_download.py +384 -0
- package/training/adapter/src/model_contract.py +38 -5
- package/training/adapter/src/prefetch.py +26 -32
- package/training/adapter/src/prefetch_dataset.py +116 -0
- package/training/adapter/src/serve.py +42 -12
- package/training/adapter/src/train.py +8 -0
- package/training/decision/pyproject.toml +25 -0
- package/training/decision/src/common.py +127 -0
- package/training/decision/src/evaluate.py +70 -0
- package/training/decision/src/train.py +181 -0
- package/training/decision/uv.lock +1175 -0
- package/dist/chunk-4Q54NHKR.js.map +0 -1
|
@@ -615,23 +615,52 @@ var TRAINING_MODELS = [
|
|
|
615
615
|
gradientCheckpointing: true
|
|
616
616
|
}
|
|
617
617
|
];
|
|
618
|
-
|
|
618
|
+
var HUGGING_FACE_REPO_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/;
|
|
619
|
+
function uncertifiedTrainingModel(id) {
|
|
620
|
+
return {
|
|
621
|
+
id,
|
|
622
|
+
family: "uncertified",
|
|
623
|
+
modelLoader: "causal_lm",
|
|
624
|
+
defaultLearningRate: 1e-4,
|
|
625
|
+
defaultPerDeviceBatchSize: 1,
|
|
626
|
+
defaultGradientAccumulationSteps: 8,
|
|
627
|
+
defaultLoraRank: 16,
|
|
628
|
+
defaultLoraAlpha: 32,
|
|
629
|
+
defaultLoraDropout: 0.05,
|
|
630
|
+
defaultMaxSeqLength: 2048,
|
|
631
|
+
loraTargetModules: "all-linear",
|
|
632
|
+
gradientCheckpointing: true
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function findCertifiedTrainingModel(modelId) {
|
|
619
636
|
const normalized = modelId.trim().toLowerCase();
|
|
620
|
-
|
|
621
|
-
|
|
637
|
+
return TRAINING_MODELS.find((candidate) => candidate.id.toLowerCase() === normalized);
|
|
638
|
+
}
|
|
639
|
+
function isCertifiedTrainingModel(modelId) {
|
|
640
|
+
return findCertifiedTrainingModel(modelId) !== void 0;
|
|
641
|
+
}
|
|
642
|
+
function resolveTrainingModel(modelId) {
|
|
643
|
+
const model = findCertifiedTrainingModel(modelId);
|
|
644
|
+
if (model) return model;
|
|
645
|
+
const id = modelId.trim();
|
|
646
|
+
if (HUGGING_FACE_REPO_ID.test(id)) return uncertifiedTrainingModel(id);
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Unsupported base model "${modelId}". Use a certified model (${TRAINING_MODELS.map((item) => item.id).join(", ")}) or a Hugging Face repo id such as owner/name.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
function uncertifiedRevisionError(modelId) {
|
|
652
|
+
return new Error(
|
|
653
|
+
`${modelId} is not a TT-certified base model. To fine-tune it anyway, set hyperparameters.base_model_revision to its 40-character commit SHA (see https://huggingface.co/${modelId}/commits/main). Uncertified models load as text causal LMs with safetensors weights and no remote code.`
|
|
622
654
|
);
|
|
623
|
-
if (!model) {
|
|
624
|
-
throw new Error(
|
|
625
|
-
`Unsupported base model "${modelId}". Supported models: ${TRAINING_MODELS.map((item) => item.id).join(", ")}`
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
return model;
|
|
629
655
|
}
|
|
630
656
|
function canonicalizeTrainingModel(modelId) {
|
|
631
657
|
return resolveTrainingModel(modelId).id;
|
|
632
658
|
}
|
|
633
659
|
function resolveRequestedBaseModelRevision(modelId, requestedRevision) {
|
|
634
660
|
const model = resolveTrainingModel(modelId);
|
|
661
|
+
if (!isCertifiedTrainingModel(model.id) && !requestedRevision) {
|
|
662
|
+
throw uncertifiedRevisionError(model.id);
|
|
663
|
+
}
|
|
635
664
|
if (model.defaultRevision && requestedRevision && requestedRevision !== model.defaultRevision) {
|
|
636
665
|
throw new Error(
|
|
637
666
|
`${model.id} must use certified revision ${model.defaultRevision}; got ${requestedRevision}.`
|
|
@@ -640,7 +669,7 @@ function resolveRequestedBaseModelRevision(modelId, requestedRevision) {
|
|
|
640
669
|
return requestedRevision ?? model.defaultRevision;
|
|
641
670
|
}
|
|
642
671
|
function defaultBaseModelRevision(modelId) {
|
|
643
|
-
return
|
|
672
|
+
return resolveTrainingModel(modelId).defaultRevision;
|
|
644
673
|
}
|
|
645
674
|
function resolveModelLoader(modelId) {
|
|
646
675
|
return resolveTrainingModel(modelId).modelLoader;
|
|
@@ -703,6 +732,7 @@ function assertCertifiedBaseModelConfig(value, label = "base-model config.json",
|
|
|
703
732
|
throw new Error(`${label} must contain a JSON object.`);
|
|
704
733
|
}
|
|
705
734
|
const config = value;
|
|
735
|
+
if (expectedModelId && !isCertifiedTrainingModel(expectedModelId)) return;
|
|
706
736
|
const expectedFamily = expectedModelId ? resolveTrainingModel(expectedModelId).family : void 0;
|
|
707
737
|
if (config.model_type === "qwen3_5") {
|
|
708
738
|
if (expectedFamily && expectedFamily !== "qwen3_5") {
|
|
@@ -846,6 +876,7 @@ var TUNED_TENSOR_DIR = ".tuned-tensor";
|
|
|
846
876
|
var DEFAULT_ARTIFACT_ROOT = `${TUNED_TENSOR_DIR}/artifacts`;
|
|
847
877
|
var DEFAULT_PROJECT_STORE_ROOT = `${TUNED_TENSOR_DIR}/store`;
|
|
848
878
|
var DEFAULT_FOUNDATION_RUNS_DIR = `${TUNED_TENSOR_DIR}/foundation-runs`;
|
|
879
|
+
var DEFAULT_DECISION_RUNS_DIR = `${TUNED_TENSOR_DIR}/decision-runs`;
|
|
849
880
|
function envHome(env = process.env) {
|
|
850
881
|
const home = env.HOME?.trim();
|
|
851
882
|
return home ? resolve3(home) : homedir();
|
|
@@ -902,6 +933,38 @@ function pythonEnvironmentPath(kind, hash, env = process.env) {
|
|
|
902
933
|
return next;
|
|
903
934
|
}
|
|
904
935
|
|
|
936
|
+
// src/local-runtime/decision-models.ts
|
|
937
|
+
var LAYA_REVISION = "55cf4c4ebb4ebe31b2550e8bdf3bd21b99753851";
|
|
938
|
+
var DECISION_MODELS = [
|
|
939
|
+
{
|
|
940
|
+
id: "convaiinnovations/laya",
|
|
941
|
+
family: "laya",
|
|
942
|
+
revision: LAYA_REVISION,
|
|
943
|
+
parameters: "421M (ModernBERT-large encoder + typed decision head)",
|
|
944
|
+
license: "Apache-2.0",
|
|
945
|
+
defaultLearningRate: 2e-5,
|
|
946
|
+
defaultBatchSize: 8,
|
|
947
|
+
defaultEpochs: 3
|
|
948
|
+
}
|
|
949
|
+
];
|
|
950
|
+
var DEFAULT_DECISION_MODEL = DECISION_MODELS[0].id;
|
|
951
|
+
function resolveDecisionModel(modelId) {
|
|
952
|
+
const normalized = modelId.trim().toLowerCase();
|
|
953
|
+
const model = DECISION_MODELS.find((candidate) => candidate.id.toLowerCase() === normalized);
|
|
954
|
+
if (!model) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`Unsupported decision model "${modelId}". Supported decision models: ${DECISION_MODELS.map((item) => item.id).join(", ")}`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
return model;
|
|
960
|
+
}
|
|
961
|
+
function isDecisionModel(modelId) {
|
|
962
|
+
return DECISION_MODELS.some((candidate) => candidate.id.toLowerCase() === modelId.trim().toLowerCase());
|
|
963
|
+
}
|
|
964
|
+
function canonicalizeDecisionModel(modelId) {
|
|
965
|
+
return resolveDecisionModel(modelId).id;
|
|
966
|
+
}
|
|
967
|
+
|
|
905
968
|
// src/local-runtime/contracts.ts
|
|
906
969
|
var datasetFormatSchema = z.literal("chat_jsonl");
|
|
907
970
|
var baseModelRevisionSchema = z.string().regex(/^[0-9a-f]{40}$/i, "base_model_revision must be a 40-character Hugging Face commit SHA").transform((value) => value.toLowerCase());
|
|
@@ -947,11 +1010,34 @@ var foundationHyperparametersSchema = z.object({
|
|
|
947
1010
|
keep_checkpoints: z.number().int().min(1).max(100).optional(),
|
|
948
1011
|
log_interval_steps: z.number().int().min(1).max(1e6).optional()
|
|
949
1012
|
}).strict();
|
|
1013
|
+
var huggingFaceRepoIdSchema = z.string().regex(
|
|
1014
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/,
|
|
1015
|
+
"huggingface.repo must be a Hugging Face dataset id such as org/name"
|
|
1016
|
+
);
|
|
1017
|
+
var datasetColumnsSchema = z.object({
|
|
1018
|
+
input: z.string().min(1),
|
|
1019
|
+
output: z.string().min(1)
|
|
1020
|
+
}).strict();
|
|
1021
|
+
var huggingFaceDatasetSourceSchema = z.object({
|
|
1022
|
+
repo: huggingFaceRepoIdSchema,
|
|
1023
|
+
revision: baseModelRevisionSchema.optional(),
|
|
1024
|
+
/**
|
|
1025
|
+
* Field names for plain JSONL records such as `{"text": ..., "label": ...}`.
|
|
1026
|
+
* Each record becomes a chat row with the spec's compiled system message.
|
|
1027
|
+
* Omit for files that are already TT chat JSONL.
|
|
1028
|
+
*/
|
|
1029
|
+
columns: datasetColumnsSchema.optional()
|
|
1030
|
+
}).strict();
|
|
1031
|
+
function isRepoRelativePath(value) {
|
|
1032
|
+
if (/^(?:[A-Za-z]:|[/\\]|file:)/.test(value)) return false;
|
|
1033
|
+
return value.split(/[/\\]/).every((part) => part !== "" && part !== "." && part !== "..");
|
|
1034
|
+
}
|
|
950
1035
|
var datasetPrebuiltSchema = z.object({
|
|
951
1036
|
training: z.string().min(1),
|
|
952
1037
|
validation: z.string().min(1).optional(),
|
|
953
1038
|
test: z.string().min(1).optional(),
|
|
954
|
-
format: datasetFormatSchema.default("chat_jsonl")
|
|
1039
|
+
format: datasetFormatSchema.default("chat_jsonl"),
|
|
1040
|
+
huggingface: huggingFaceDatasetSourceSchema.optional()
|
|
955
1041
|
}).strict().superRefine((dataset, context) => {
|
|
956
1042
|
if (!dataset.validation && !dataset.test) {
|
|
957
1043
|
context.addIssue({
|
|
@@ -960,7 +1046,57 @@ var datasetPrebuiltSchema = z.object({
|
|
|
960
1046
|
message: "Provide a distinct validation or test split for held-out evaluation"
|
|
961
1047
|
});
|
|
962
1048
|
}
|
|
1049
|
+
if (!dataset.huggingface) return;
|
|
1050
|
+
for (const key of ["training", "validation", "test"]) {
|
|
1051
|
+
const value = dataset[key];
|
|
1052
|
+
if (value === void 0) continue;
|
|
1053
|
+
if (!isRepoRelativePath(value)) {
|
|
1054
|
+
context.addIssue({
|
|
1055
|
+
code: z.ZodIssueCode.custom,
|
|
1056
|
+
path: [key],
|
|
1057
|
+
message: `With dataset_prebuilt.huggingface, ${key} must be a file path inside the dataset repo, such as data/${key}.jsonl`
|
|
1058
|
+
});
|
|
1059
|
+
} else if (!value.toLowerCase().endsWith(".jsonl")) {
|
|
1060
|
+
context.addIssue({
|
|
1061
|
+
code: z.ZodIssueCode.custom,
|
|
1062
|
+
path: [key],
|
|
1063
|
+
message: `${key} must be a chat JSONL (.jsonl) file; Parquet, CSV and other formats are not supported yet`
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
963
1067
|
});
|
|
1068
|
+
var datasetSourceFileSchema = z.object({
|
|
1069
|
+
path: z.string().min(1),
|
|
1070
|
+
size_bytes: z.number().int().nonnegative(),
|
|
1071
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/)
|
|
1072
|
+
}).strict();
|
|
1073
|
+
var datasetConversionStatsSchema = z.object({
|
|
1074
|
+
records: z.number().int().nonnegative(),
|
|
1075
|
+
kept: z.number().int().nonnegative(),
|
|
1076
|
+
skipped_missing_fields: z.number().int().nonnegative(),
|
|
1077
|
+
dropped_duplicate_inputs: z.number().int().nonnegative(),
|
|
1078
|
+
dropped_eval_overlap: z.number().int().nonnegative()
|
|
1079
|
+
}).strict();
|
|
1080
|
+
var datasetSourceSchema = z.object({
|
|
1081
|
+
provider: z.literal("huggingface"),
|
|
1082
|
+
repo: huggingFaceRepoIdSchema,
|
|
1083
|
+
requested_revision: baseModelRevisionSchema.nullable(),
|
|
1084
|
+
revision: baseModelRevisionSchema,
|
|
1085
|
+
pinned: z.boolean(),
|
|
1086
|
+
columns: datasetColumnsSchema.nullable(),
|
|
1087
|
+
/** Per-split record accounting when `columns` converted plain JSONL records. */
|
|
1088
|
+
conversion: z.object({
|
|
1089
|
+
training: datasetConversionStatsSchema,
|
|
1090
|
+
validation: datasetConversionStatsSchema.optional(),
|
|
1091
|
+
test: datasetConversionStatsSchema.optional()
|
|
1092
|
+
}).strict().nullable().optional(),
|
|
1093
|
+
snapshot_path: z.string().min(1),
|
|
1094
|
+
files: z.object({
|
|
1095
|
+
training: datasetSourceFileSchema,
|
|
1096
|
+
validation: datasetSourceFileSchema.optional(),
|
|
1097
|
+
test: datasetSourceFileSchema.optional()
|
|
1098
|
+
}).strict()
|
|
1099
|
+
}).strict();
|
|
964
1100
|
var fineTuneHyperparametersSchema = z.object({
|
|
965
1101
|
n_epochs: z.number().int().min(1).max(20).default(1),
|
|
966
1102
|
learning_rate: z.number().positive().optional(),
|
|
@@ -974,6 +1110,15 @@ var fineTuneHyperparametersSchema = z.object({
|
|
|
974
1110
|
/** Immutable Hugging Face commit used by every stage of the run. */
|
|
975
1111
|
base_model_revision: baseModelRevisionSchema.optional()
|
|
976
1112
|
}).strict();
|
|
1113
|
+
function requirePinnedUncertifiedModel(spec, context) {
|
|
1114
|
+
const baseModel = "spec_snapshot" in spec ? spec.spec_snapshot.base_model : spec.base_model;
|
|
1115
|
+
if (isCertifiedTrainingModel(baseModel) || spec.hyperparameters?.base_model_revision) return;
|
|
1116
|
+
context.addIssue({
|
|
1117
|
+
code: z.ZodIssueCode.custom,
|
|
1118
|
+
path: ["hyperparameters", "base_model_revision"],
|
|
1119
|
+
message: uncertifiedRevisionError(baseModel).message
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
977
1122
|
var fineTuneRunRequestSchema = z.object({
|
|
978
1123
|
run_id: z.string().uuid(),
|
|
979
1124
|
user_id: z.string().min(1),
|
|
@@ -981,8 +1126,10 @@ var fineTuneRunRequestSchema = z.object({
|
|
|
981
1126
|
run_number: z.number().int().min(1),
|
|
982
1127
|
spec_snapshot: specSnapshotSchema,
|
|
983
1128
|
hyperparameters: fineTuneHyperparametersSchema.default({ n_epochs: 1 }),
|
|
984
|
-
dataset_prebuilt: datasetPrebuiltSchema.optional()
|
|
1129
|
+
dataset_prebuilt: datasetPrebuiltSchema.optional(),
|
|
1130
|
+
dataset_source: datasetSourceSchema.optional()
|
|
985
1131
|
}).strict().superRefine((request, context) => {
|
|
1132
|
+
requirePinnedUncertifiedModel(request, context);
|
|
986
1133
|
if (request.spec_snapshot.examples.length === 0 && !request.dataset_prebuilt) {
|
|
987
1134
|
context.addIssue({
|
|
988
1135
|
code: z.ZodIssueCode.custom,
|
|
@@ -1070,6 +1217,55 @@ var trainingReportSchema = z.object({
|
|
|
1070
1217
|
log_uri: z.string(),
|
|
1071
1218
|
command: z.array(z.string()).optional()
|
|
1072
1219
|
}).strict();
|
|
1220
|
+
var runProvenanceSchema = z.object({
|
|
1221
|
+
schema_version: z.literal(1),
|
|
1222
|
+
recorded_at: z.string(),
|
|
1223
|
+
software: z.object({
|
|
1224
|
+
tt_version: z.string(),
|
|
1225
|
+
node_version: z.string(),
|
|
1226
|
+
platform: z.string(),
|
|
1227
|
+
arch: z.string(),
|
|
1228
|
+
runtime_fingerprint: z.string(),
|
|
1229
|
+
python_lock_sha256: z.string().nullable()
|
|
1230
|
+
}).strict(),
|
|
1231
|
+
execution: z.object({
|
|
1232
|
+
target: z.enum(["local", "aws"]),
|
|
1233
|
+
training_backend: z.string(),
|
|
1234
|
+
instance_id: z.string().optional(),
|
|
1235
|
+
region: z.string().optional()
|
|
1236
|
+
}).strict(),
|
|
1237
|
+
spec: z.object({
|
|
1238
|
+
behavior_spec_id: z.string(),
|
|
1239
|
+
run_number: z.number().int(),
|
|
1240
|
+
snapshot_sha256: z.string(),
|
|
1241
|
+
system_prompt_sha256: z.string(),
|
|
1242
|
+
file_path: z.string().optional(),
|
|
1243
|
+
file_sha256: z.string().optional()
|
|
1244
|
+
}).strict(),
|
|
1245
|
+
base_model: z.object({
|
|
1246
|
+
id: z.string(),
|
|
1247
|
+
revision: z.string().nullable(),
|
|
1248
|
+
fingerprint: z.string().nullable()
|
|
1249
|
+
}).strict(),
|
|
1250
|
+
data: z.object({
|
|
1251
|
+
prebuilt: z.boolean(),
|
|
1252
|
+
format: z.string().nullable(),
|
|
1253
|
+
file_sha256: z.record(z.string(), z.string()),
|
|
1254
|
+
compiled_training_sha256: z.string().nullable(),
|
|
1255
|
+
training_examples: z.number().int().nonnegative().nullable(),
|
|
1256
|
+
eval_split: z.string(),
|
|
1257
|
+
eval_sample_seed: z.number().int(),
|
|
1258
|
+
eval_examples_total: z.number().int().nonnegative(),
|
|
1259
|
+
eval_examples_used: z.number().int().nonnegative()
|
|
1260
|
+
}).strict(),
|
|
1261
|
+
training: z.object({
|
|
1262
|
+
hyperparameters: z.record(z.string(), z.unknown()),
|
|
1263
|
+
exit_code: z.number().int().nullable(),
|
|
1264
|
+
log_uri: z.string()
|
|
1265
|
+
}).strict(),
|
|
1266
|
+
/** Hash over request, runtime, preparation config and input digests. */
|
|
1267
|
+
source_fingerprint: z.string()
|
|
1268
|
+
}).strict();
|
|
1073
1269
|
var runReportSchema = z.object({
|
|
1074
1270
|
run_id: z.string().uuid(),
|
|
1075
1271
|
behavior_spec_id: z.string().uuid(),
|
|
@@ -1095,6 +1291,7 @@ var runReportSchema = z.object({
|
|
|
1095
1291
|
failures: z.array(z.string())
|
|
1096
1292
|
}).strict().optional(),
|
|
1097
1293
|
training: trainingReportSchema,
|
|
1294
|
+
provenance: runProvenanceSchema.optional(),
|
|
1098
1295
|
artifact_uris: z.object({
|
|
1099
1296
|
dataset: z.string(),
|
|
1100
1297
|
baseline_eval: z.string(),
|
|
@@ -1109,6 +1306,7 @@ var runReportSchema = z.object({
|
|
|
1109
1306
|
dataset_prebuilt: z.boolean(),
|
|
1110
1307
|
dataset_format: datasetFormatSchema.nullable().optional(),
|
|
1111
1308
|
dataset_uri: z.string(),
|
|
1309
|
+
dataset_source: datasetSourceSchema.optional(),
|
|
1112
1310
|
spec_example_count: z.number().int().nonnegative(),
|
|
1113
1311
|
training_example_count: z.number().int().nonnegative().nullable(),
|
|
1114
1312
|
eval_examples_total: z.number().int().nonnegative(),
|
|
@@ -1205,9 +1403,54 @@ var localAdapterSpecFileSchema = specSnapshotSchema.extend({
|
|
|
1205
1403
|
id: z.string().uuid().optional(),
|
|
1206
1404
|
hyperparameters: fineTuneHyperparametersSchema.optional(),
|
|
1207
1405
|
dataset_prebuilt: datasetPrebuiltSchema.optional()
|
|
1406
|
+
}).strict().superRefine(requirePinnedUncertifiedModel);
|
|
1407
|
+
var decisionQuestionSchema = z.discriminatedUnion("type", [
|
|
1408
|
+
z.object({
|
|
1409
|
+
type: z.literal("choice"),
|
|
1410
|
+
/** Answer label -> description the model reads. Key order is option order. */
|
|
1411
|
+
criteria: z.record(z.string().min(1), z.string()).refine(
|
|
1412
|
+
(criteria) => Object.keys(criteria).length >= 2 && Object.keys(criteria).length <= 32,
|
|
1413
|
+
"A choice decision needs 2 to 32 labels."
|
|
1414
|
+
)
|
|
1415
|
+
}).strict(),
|
|
1416
|
+
z.object({
|
|
1417
|
+
type: z.literal("noul"),
|
|
1418
|
+
criteria: z.object({ true: z.string().optional(), false: z.string().optional() }).strict().optional()
|
|
1419
|
+
}).strict(),
|
|
1420
|
+
z.object({
|
|
1421
|
+
type: z.literal("score"),
|
|
1422
|
+
/** Level descriptions, index 0 first; example outputs are level indices. */
|
|
1423
|
+
criteria: z.array(z.string().min(1)).min(2).max(10)
|
|
1424
|
+
}).strict()
|
|
1425
|
+
]);
|
|
1426
|
+
var decisionHyperparametersSchema = z.object({
|
|
1427
|
+
n_epochs: z.number().int().min(1).max(50).optional(),
|
|
1428
|
+
learning_rate: z.number().positive().max(1).optional(),
|
|
1429
|
+
batch_size: z.number().int().min(1).max(256).optional(),
|
|
1430
|
+
/** Train only the decision head; faster on CPU, usually less accurate. */
|
|
1431
|
+
freeze_encoder: z.boolean().optional(),
|
|
1432
|
+
/** Shuffle option order per training row so the head learns meaning, not position. */
|
|
1433
|
+
shuffle_options: z.boolean().optional(),
|
|
1434
|
+
seed: z.number().int().min(0).max(2147483647).optional(),
|
|
1435
|
+
device: z.enum(["auto", "cpu", "cuda", "mps"]).optional()
|
|
1208
1436
|
}).strict();
|
|
1437
|
+
var localDecisionSpecFileSchema = behaviorSpecSchema.extend({
|
|
1438
|
+
...projectWorkflowFields,
|
|
1439
|
+
engine: z.literal("decision"),
|
|
1440
|
+
id: z.string().uuid().optional(),
|
|
1441
|
+
// Refine first: a union member must report an issue, not throw, for other engines' specs.
|
|
1442
|
+
base_model: z.string().refine(isDecisionModel, `Unsupported decision model. Supported decision models: ${DECISION_MODELS.map((model) => model.id).join(", ")}`).transform((value) => canonicalizeDecisionModel(value)),
|
|
1443
|
+
decision: decisionQuestionSchema,
|
|
1444
|
+
hyperparameters: decisionHyperparametersSchema.optional()
|
|
1445
|
+
});
|
|
1446
|
+
function decisionLabels(question) {
|
|
1447
|
+
if (question.type === "choice") return Object.keys(question.criteria);
|
|
1448
|
+
if (question.type === "noul") return ["false", "true"];
|
|
1449
|
+
return question.criteria.map((_, index) => String(index));
|
|
1450
|
+
}
|
|
1209
1451
|
var localBehaviorSpecFileSchema = z.union([
|
|
1210
1452
|
localFoundationSpecFileSchema,
|
|
1453
|
+
localDecisionSpecFileSchema,
|
|
1211
1454
|
localAdapterSpecFileSchema
|
|
1212
1455
|
]).superRefine((spec, ctx) => {
|
|
1213
1456
|
if (spec.pipeline && isFoundationPipeline(parsePipeline(spec.pipeline)) !== (spec.engine === "foundation")) {
|
|
@@ -1216,14 +1459,145 @@ var localBehaviorSpecFileSchema = z.union([
|
|
|
1216
1459
|
if (spec.engine === "foundation" && (spec.evaluation || spec.runtime?.storeRoot || spec.runtime?.paths)) {
|
|
1217
1460
|
ctx.addIssue({ code: "custom", path: ["runtime"], message: "Foundation uses foundation settings for evaluation and checkpoints; runtime.artifactRoot and runtime.gpu are supported." });
|
|
1218
1461
|
}
|
|
1462
|
+
if (spec.engine === "decision" && (spec.evaluation || spec.runtime?.gpu || spec.runtime?.storeRoot || spec.runtime?.paths)) {
|
|
1463
|
+
ctx.addIssue({ code: "custom", path: ["runtime"], message: "Decision specs support runtime.artifactRoot only; the device is set in hyperparameters.device." });
|
|
1464
|
+
}
|
|
1219
1465
|
});
|
|
1220
1466
|
function isFoundationSpecFile(spec) {
|
|
1221
1467
|
return spec.engine === "foundation";
|
|
1222
1468
|
}
|
|
1469
|
+
function isDecisionSpecFile(spec) {
|
|
1470
|
+
return spec.engine === "decision";
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/local-runtime/dataset.ts
|
|
1474
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1475
|
+
async function loadNormalizedChatJsonl(path) {
|
|
1476
|
+
const text = await readFile2(path, "utf8");
|
|
1477
|
+
const rows = [];
|
|
1478
|
+
const jsonLines = [];
|
|
1479
|
+
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
1480
|
+
if (!line.trim()) continue;
|
|
1481
|
+
let value;
|
|
1482
|
+
try {
|
|
1483
|
+
value = JSON.parse(line);
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
throw new Error(`Invalid chat JSONL row ${index + 1}: malformed JSON`, { cause: error });
|
|
1486
|
+
}
|
|
1487
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1488
|
+
throw new Error(`Invalid chat JSONL row ${index + 1}: expected an object`);
|
|
1489
|
+
}
|
|
1490
|
+
const messages = value.messages;
|
|
1491
|
+
if (!Array.isArray(messages) || ![2, 3].includes(messages.length) || messages.some(
|
|
1492
|
+
(message) => !message || typeof message !== "object" || Array.isArray(message) || !["system", "user", "assistant"].includes(String(message.role)) || typeof message.content !== "string"
|
|
1493
|
+
)) {
|
|
1494
|
+
throw new Error(
|
|
1495
|
+
`Invalid chat JSONL row ${index + 1}: expected an optional system message, one user message, and one assistant answer`
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
const row = { messages };
|
|
1499
|
+
const offset = row.messages.length === 3 ? 1 : 0;
|
|
1500
|
+
if (offset === 1 && row.messages[0]?.role !== "system" || row.messages[offset]?.role !== "user" || !row.messages[offset]?.content.trim() || row.messages[offset + 1]?.role !== "assistant" || !row.messages[offset + 1]?.content.trim()) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`Invalid chat JSONL row ${index + 1}: expected an optional system message, one non-empty user message, and one non-empty assistant answer`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
rows.push(row);
|
|
1506
|
+
jsonLines.push(JSON.stringify(row));
|
|
1507
|
+
}
|
|
1508
|
+
if (rows.length === 0) throw new Error(`Chat JSONL contains no examples: ${path}`);
|
|
1509
|
+
return {
|
|
1510
|
+
rows,
|
|
1511
|
+
jsonl: jsonLines.join("\n")
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
async function normalizeChatJsonlForRelocation(path) {
|
|
1515
|
+
return (await loadNormalizedChatJsonl(path)).jsonl;
|
|
1516
|
+
}
|
|
1517
|
+
function datasetInputIdentity(value) {
|
|
1518
|
+
return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
|
|
1519
|
+
}
|
|
1520
|
+
function buildSystemMessage(spec) {
|
|
1521
|
+
const parts = [];
|
|
1522
|
+
const guidelines = spec.guidelines.map((value) => value.trim()).filter(Boolean);
|
|
1523
|
+
const constraints = spec.constraints.map((value) => value.trim()).filter(Boolean);
|
|
1524
|
+
if (spec.system_prompt.trim()) parts.push(spec.system_prompt.trim());
|
|
1525
|
+
if (guidelines.length > 0) {
|
|
1526
|
+
parts.push(`Guidelines:
|
|
1527
|
+
${guidelines.map((guideline) => `- ${guideline}`).join("\n")}`);
|
|
1528
|
+
}
|
|
1529
|
+
if (constraints.length > 0) {
|
|
1530
|
+
parts.push(`Constraints:
|
|
1531
|
+
${constraints.map((constraint) => `- ${constraint}`).join("\n")}`);
|
|
1532
|
+
}
|
|
1533
|
+
return parts.join("\n\n") || "Follow the demonstrated behavior.";
|
|
1534
|
+
}
|
|
1535
|
+
function exampleToChatRow(spec, example) {
|
|
1536
|
+
const system = buildSystemMessage(spec);
|
|
1537
|
+
return {
|
|
1538
|
+
messages: [
|
|
1539
|
+
{ role: "system", content: system },
|
|
1540
|
+
{ role: "user", content: example.input },
|
|
1541
|
+
{ role: "assistant", content: example.output }
|
|
1542
|
+
]
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function compileSpecToJsonl(spec) {
|
|
1546
|
+
return spec.examples.map((example) => JSON.stringify(exampleToChatRow(spec, example))).join("\n");
|
|
1547
|
+
}
|
|
1548
|
+
function examplesFromSpec(spec) {
|
|
1549
|
+
return spec.examples;
|
|
1550
|
+
}
|
|
1551
|
+
async function examplesFromChatJsonl(path) {
|
|
1552
|
+
const normalized = await loadNormalizedChatJsonl(path);
|
|
1553
|
+
const examples = [];
|
|
1554
|
+
for (const [index, row] of normalized.rows.entries()) {
|
|
1555
|
+
const user = row.messages.at(-2);
|
|
1556
|
+
const assistant = row.messages.at(-1);
|
|
1557
|
+
examples.push({
|
|
1558
|
+
input: user.content,
|
|
1559
|
+
output: assistant.content
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
return examples;
|
|
1563
|
+
}
|
|
1564
|
+
async function evaluationSuiteFromChatJsonl(path, configuredSystem) {
|
|
1565
|
+
const normalized = await loadNormalizedChatJsonl(path);
|
|
1566
|
+
const inputIdentities = /* @__PURE__ */ new Set();
|
|
1567
|
+
for (const row of normalized.rows) {
|
|
1568
|
+
const input = row.messages.at(-2).content;
|
|
1569
|
+
const identity = input.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1570
|
+
if (inputIdentities.has(identity)) {
|
|
1571
|
+
throw new Error(
|
|
1572
|
+
"General regression data contains duplicate inputs; evaluation prompts must be unique."
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
inputIdentities.add(identity);
|
|
1576
|
+
}
|
|
1577
|
+
const systems = new Set(
|
|
1578
|
+
normalized.rows.map((row) => row.messages.find((message) => message.role === "system")?.content).filter((value) => value !== void 0)
|
|
1579
|
+
);
|
|
1580
|
+
if (systems.size > 1) {
|
|
1581
|
+
throw new Error("General regression rows must use one consistent system message.");
|
|
1582
|
+
}
|
|
1583
|
+
const embeddedSystem = [...systems][0];
|
|
1584
|
+
if (configuredSystem !== void 0 && embeddedSystem !== void 0 && configuredSystem !== embeddedSystem) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
"evaluation.generalRegression.systemPrompt must match the system message embedded in the dataset."
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
examples: normalized.rows.map((row) => ({
|
|
1591
|
+
input: row.messages.at(-2).content,
|
|
1592
|
+
output: row.messages.at(-1).content
|
|
1593
|
+
})),
|
|
1594
|
+
system: configuredSystem ?? embeddedSystem ?? "You are a helpful assistant."
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1223
1597
|
|
|
1224
1598
|
// src/local-runtime/local-project.ts
|
|
1225
1599
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
1226
|
-
import { readFile as
|
|
1600
|
+
import { readFile as readFile3, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1227
1601
|
import { dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
|
|
1228
1602
|
import { homedir as homedir2 } from "os";
|
|
1229
1603
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
@@ -1360,7 +1734,7 @@ function resolveLocalRunInputPaths(raw, inputPath) {
|
|
|
1360
1734
|
const dataset = value.dataset_prebuilt;
|
|
1361
1735
|
if (dataset && typeof dataset === "object" && !Array.isArray(dataset)) {
|
|
1362
1736
|
const fields = dataset;
|
|
1363
|
-
for (const key of ["training", "validation", "test"]) {
|
|
1737
|
+
if (fields.huggingface === void 0) for (const key of ["training", "validation", "test"]) {
|
|
1364
1738
|
if (fields[key] !== void 0) fields[key] = resolveLocalReference(fields[key], baseDirectory);
|
|
1365
1739
|
}
|
|
1366
1740
|
}
|
|
@@ -1432,6 +1806,34 @@ async function initLocalSpecFile(args) {
|
|
|
1432
1806
|
});
|
|
1433
1807
|
await mkdir2(dirname2(args.outputPath), { recursive: true });
|
|
1434
1808
|
await writeFile2(args.outputPath, `${JSON.stringify(spec2, null, 2)}
|
|
1809
|
+
`, "utf8");
|
|
1810
|
+
return spec2;
|
|
1811
|
+
}
|
|
1812
|
+
if (args.engine === "decision") {
|
|
1813
|
+
const spec2 = localDecisionSpecFileSchema.parse({
|
|
1814
|
+
engine: "decision",
|
|
1815
|
+
id: randomUUID2(),
|
|
1816
|
+
name: args.name,
|
|
1817
|
+
description: "Typed decision model: one labelled answer per input.",
|
|
1818
|
+
system_prompt: "Describe the decision this local model should make.",
|
|
1819
|
+
guidelines: [],
|
|
1820
|
+
constraints: [],
|
|
1821
|
+
base_model: args.baseModel ?? DEFAULT_DECISION_MODEL,
|
|
1822
|
+
decision: {
|
|
1823
|
+
type: "choice",
|
|
1824
|
+
criteria: {
|
|
1825
|
+
first_label: "Replace this with what the first label means.",
|
|
1826
|
+
second_label: "Replace this with what the second label means."
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
examples: [
|
|
1830
|
+
{ input: "Replace this with a representative input.", output: "first_label" },
|
|
1831
|
+
{ input: "Replace this with a different input.", output: "second_label" }
|
|
1832
|
+
],
|
|
1833
|
+
hyperparameters: { n_epochs: 3 }
|
|
1834
|
+
});
|
|
1835
|
+
await mkdir2(dirname2(args.outputPath), { recursive: true });
|
|
1836
|
+
await writeFile2(args.outputPath, `${JSON.stringify(spec2, null, 2)}
|
|
1435
1837
|
`, "utf8");
|
|
1436
1838
|
return spec2;
|
|
1437
1839
|
}
|
|
@@ -1491,7 +1893,7 @@ async function initLocalRunnerConfigFile(args) {
|
|
|
1491
1893
|
}
|
|
1492
1894
|
function placeholderIssues(systemPrompt, examples) {
|
|
1493
1895
|
const issues = [];
|
|
1494
|
-
if (/describe the behavior this local model should learn/i.test(systemPrompt)) {
|
|
1896
|
+
if (/describe the (behavior|decision) this local model should (learn|make)/i.test(systemPrompt)) {
|
|
1495
1897
|
issues.push("system_prompt still contains the generated placeholder");
|
|
1496
1898
|
}
|
|
1497
1899
|
examples.forEach((example, index) => {
|
|
@@ -1513,6 +1915,14 @@ function assertLocalRunInputReady(request) {
|
|
|
1513
1915
|
function foundationPlaceholderIssues(spec) {
|
|
1514
1916
|
return placeholderIssues(spec.system_prompt, spec.examples);
|
|
1515
1917
|
}
|
|
1918
|
+
function decisionPlaceholderIssues(spec) {
|
|
1919
|
+
const issues = placeholderIssues(spec.system_prompt, spec.examples);
|
|
1920
|
+
const descriptions = spec.decision.type === "noul" ? Object.values(spec.decision.criteria ?? {}) : Object.values(spec.decision.criteria);
|
|
1921
|
+
if (descriptions.some((value) => /replace this with/i.test(value ?? ""))) {
|
|
1922
|
+
issues.push("decision.criteria still contains generated placeholder text");
|
|
1923
|
+
}
|
|
1924
|
+
return issues;
|
|
1925
|
+
}
|
|
1516
1926
|
function assertFoundationSpecReady(spec) {
|
|
1517
1927
|
const issues = foundationPlaceholderIssues(spec);
|
|
1518
1928
|
if (issues.length > 0) {
|
|
@@ -1520,9 +1930,9 @@ function assertFoundationSpecReady(spec) {
|
|
|
1520
1930
|
}
|
|
1521
1931
|
}
|
|
1522
1932
|
function runRequestFromLocalSpec(spec, options = {}) {
|
|
1523
|
-
if (isFoundationSpecFile(spec)) {
|
|
1933
|
+
if (isFoundationSpecFile(spec) || isDecisionSpecFile(spec)) {
|
|
1524
1934
|
throw new Error(
|
|
1525
|
-
"Foundation specs cannot be converted into a LoRA run request. Use
|
|
1935
|
+
`${isFoundationSpecFile(spec) ? "Foundation" : "Decision"} specs cannot be converted into a LoRA run request. Use \`tt pipeline run --spec\`.`
|
|
1526
1936
|
);
|
|
1527
1937
|
}
|
|
1528
1938
|
const {
|
|
@@ -1570,6 +1980,9 @@ function parseLocalRunInput(rawInput, path, options = {}) {
|
|
|
1570
1980
|
if (isFoundationSpecFile(spec)) {
|
|
1571
1981
|
return { kind: "foundation-spec", path, spec };
|
|
1572
1982
|
}
|
|
1983
|
+
if (isDecisionSpecFile(spec)) {
|
|
1984
|
+
return { kind: "decision-spec", path, spec };
|
|
1985
|
+
}
|
|
1573
1986
|
return {
|
|
1574
1987
|
kind: "spec",
|
|
1575
1988
|
path,
|
|
@@ -1579,7 +1992,7 @@ function parseLocalRunInput(rawInput, path, options = {}) {
|
|
|
1579
1992
|
}
|
|
1580
1993
|
async function loadLocalRunInput(path, options = {}) {
|
|
1581
1994
|
return parseLocalRunInput(
|
|
1582
|
-
JSON.parse(await
|
|
1995
|
+
JSON.parse(await readFile3(path, "utf8")),
|
|
1583
1996
|
path,
|
|
1584
1997
|
options
|
|
1585
1998
|
);
|
|
@@ -1594,7 +2007,7 @@ function validateBehaviorSpec(input) {
|
|
|
1594
2007
|
return { valid: false, errors: ["Use a behavior spec, not a run request."], warnings };
|
|
1595
2008
|
}
|
|
1596
2009
|
const { spec } = input;
|
|
1597
|
-
const placeholders = input.kind === "foundation-spec" ? foundationPlaceholderIssues(input.spec) : generatedPlaceholderIssues(input.request);
|
|
2010
|
+
const placeholders = input.kind === "foundation-spec" ? foundationPlaceholderIssues(input.spec) : input.kind === "decision-spec" ? decisionPlaceholderIssues(input.spec) : generatedPlaceholderIssues(input.request);
|
|
1598
2011
|
if (placeholders.length) {
|
|
1599
2012
|
errors.push(`Edit the generated behavior spec before training: ${placeholders.join("; ")}.`);
|
|
1600
2013
|
}
|
|
@@ -1609,6 +2022,29 @@ function validateBehaviorSpec(input) {
|
|
|
1609
2022
|
errors.push("Foundation RL requires numeric expected outputs in every example.");
|
|
1610
2023
|
}
|
|
1611
2024
|
}
|
|
2025
|
+
if (input.kind === "decision-spec") {
|
|
2026
|
+
const labels = decisionLabels(input.spec.decision);
|
|
2027
|
+
const normalized = new Map(labels.map((label) => [label.trim().toLowerCase(), label]));
|
|
2028
|
+
if (normalized.size !== labels.length) {
|
|
2029
|
+
errors.push("decision labels must stay distinct ignoring case and surrounding spaces.");
|
|
2030
|
+
}
|
|
2031
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2032
|
+
for (const [index, example] of spec.examples.entries()) {
|
|
2033
|
+
const label = normalized.get(example.output.trim().toLowerCase());
|
|
2034
|
+
if (!label) {
|
|
2035
|
+
errors.push(`examples[${index}].output must be one of the decision labels: ${labels.join(", ")}.`);
|
|
2036
|
+
} else {
|
|
2037
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const missing = labels.filter((label) => !counts.has(label));
|
|
2041
|
+
if (missing.length && spec.examples.length) {
|
|
2042
|
+
warnings.push(`No examples use decision label(s) ${missing.join(", ")}; the tuned model learns those only from their descriptions.`);
|
|
2043
|
+
}
|
|
2044
|
+
if (buildSystemMessage(spec).length > 600) {
|
|
2045
|
+
warnings.push("The compiled decision instruction exceeds 600 characters; the decision model reads about 190 tokens of instruction and options, so later text may be truncated.");
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
1612
2048
|
for (const key of ["name", "system_prompt"]) {
|
|
1613
2049
|
if (!spec[key].trim()) errors.push(`${key} must not be blank.`);
|
|
1614
2050
|
}
|
|
@@ -1784,7 +2220,7 @@ ${validation.errors.join("\n")}`);
|
|
|
1784
2220
|
const pipeline = parsePipeline2(recipe);
|
|
1785
2221
|
if (isFoundationPipeline2(pipeline) !== foundation) {
|
|
1786
2222
|
const recipeEngine = isFoundationPipeline2(pipeline) ? "a foundation" : "an adapter";
|
|
1787
|
-
const specEngine = foundation ? "a foundation" : "an adapter";
|
|
2223
|
+
const specEngine = foundation ? "a foundation" : input.kind === "decision-spec" ? "a decision" : "an adapter";
|
|
1788
2224
|
throw new Error(`Pipeline is ${recipeEngine} recipe, but the behavior spec is ${specEngine} spec. Use a matching recipe or omit --file.`);
|
|
1789
2225
|
}
|
|
1790
2226
|
if (foundation) assertFoundationSettingsMatch(input.spec.foundation, pipeline);
|
|
@@ -1833,177 +2269,346 @@ function assertFoundationSettingsMatch(hp, plan) {
|
|
|
1833
2269
|
}
|
|
1834
2270
|
}
|
|
1835
2271
|
|
|
1836
|
-
// src/local-runtime/
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2272
|
+
// src/local-runtime/run-reporter.ts
|
|
2273
|
+
function reportInBackground(action) {
|
|
2274
|
+
try {
|
|
2275
|
+
void Promise.resolve(action()).catch(() => void 0);
|
|
2276
|
+
} catch {
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function sanitizeLogLine(line) {
|
|
2280
|
+
return line.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]").replace(/((?:api[_-]?key|token|secret|password)=)[^\s]+/gi, "$1[redacted]");
|
|
2281
|
+
}
|
|
2282
|
+
function forwardStreamLines(stream, onLine) {
|
|
2283
|
+
let buffered = "";
|
|
2284
|
+
stream.setEncoding("utf8");
|
|
2285
|
+
stream.on("data", (chunk) => {
|
|
2286
|
+
buffered += chunk;
|
|
2287
|
+
const lines = buffered.split(/\r\n|\n|\r/);
|
|
2288
|
+
buffered = lines.pop() ?? "";
|
|
2289
|
+
for (const line of lines) {
|
|
2290
|
+
if (line.trim()) onLine(sanitizeLogLine(line));
|
|
1849
2291
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
2292
|
+
});
|
|
2293
|
+
stream.on("end", () => {
|
|
2294
|
+
if (buffered.trim()) onLine(sanitizeLogLine(buffered));
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/local-runtime/package-root.ts
|
|
2299
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2300
|
+
import { dirname as dirname3, resolve as resolve5 } from "path";
|
|
2301
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2302
|
+
function localRuntimePackageRoot(moduleUrl) {
|
|
2303
|
+
const moduleDirectory = dirname3(fileURLToPath2(moduleUrl));
|
|
2304
|
+
const candidates = [
|
|
2305
|
+
dirname3(moduleDirectory),
|
|
2306
|
+
dirname3(dirname3(moduleDirectory))
|
|
2307
|
+
];
|
|
2308
|
+
const root = candidates.find(
|
|
2309
|
+
(candidate) => existsSync2(resolve5(candidate, "package.json")) && existsSync2(resolve5(candidate, "training/adapter"))
|
|
2310
|
+
);
|
|
2311
|
+
if (!root) {
|
|
2312
|
+
throw new Error("Unable to locate the bundled Tuned Tensor local runtime assets.");
|
|
2313
|
+
}
|
|
2314
|
+
return root;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// src/local-runtime/audit.ts
|
|
2318
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
2319
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
2320
|
+
import { mkdir as mkdir3, open as open2, readFile as readFile4, rename as rename2, rm, stat as stat3, writeFile as writeFile3 } from "fs/promises";
|
|
2321
|
+
import { hostname } from "os";
|
|
2322
|
+
import { join as join3 } from "path";
|
|
2323
|
+
function canonicalJson3(value) {
|
|
2324
|
+
const normalized = JSON.parse(JSON.stringify(value ?? null));
|
|
2325
|
+
const encode = (entry) => {
|
|
2326
|
+
if (entry === null || typeof entry !== "object") return JSON.stringify(entry);
|
|
2327
|
+
if (Array.isArray(entry)) return `[${entry.map(encode).join(",")}]`;
|
|
2328
|
+
const object = entry;
|
|
2329
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${encode(object[key])}`).join(",")}}`;
|
|
2330
|
+
};
|
|
2331
|
+
return encode(normalized);
|
|
2332
|
+
}
|
|
2333
|
+
function eventHash(event) {
|
|
2334
|
+
const { hash: _hash, ...body } = event;
|
|
2335
|
+
return createHash3("sha256").update(canonicalJson3(body)).digest("hex");
|
|
2336
|
+
}
|
|
2337
|
+
function chainEvent(event, previous) {
|
|
2338
|
+
const body = JSON.parse(JSON.stringify({
|
|
2339
|
+
...event,
|
|
2340
|
+
seq: typeof previous?.seq === "number" ? previous.seq + 1 : 0,
|
|
2341
|
+
prev_hash: previous?.hash ?? null
|
|
2342
|
+
}));
|
|
2343
|
+
return { ...body, hash: eventHash(body) };
|
|
2344
|
+
}
|
|
2345
|
+
function verifyEventChain(events) {
|
|
2346
|
+
let legacy = 0;
|
|
2347
|
+
let unchainedAfter = 0;
|
|
2348
|
+
let previous;
|
|
2349
|
+
const result = (status, brk) => ({
|
|
2350
|
+
status,
|
|
2351
|
+
events: events.length,
|
|
2352
|
+
chained_events: events.filter((event) => event.hash !== void 0).length,
|
|
2353
|
+
legacy_events: legacy,
|
|
2354
|
+
unchained_after_chain: unchainedAfter,
|
|
2355
|
+
head_hash: previous?.hash ?? null,
|
|
2356
|
+
...brk ? { break: brk } : {}
|
|
2357
|
+
});
|
|
2358
|
+
for (const [index, event] of events.entries()) {
|
|
2359
|
+
const chained = event;
|
|
2360
|
+
if (chained.hash === void 0) {
|
|
2361
|
+
if (previous) unchainedAfter += 1;
|
|
2362
|
+
else legacy += 1;
|
|
2363
|
+
continue;
|
|
1852
2364
|
}
|
|
1853
|
-
const
|
|
1854
|
-
if (
|
|
1855
|
-
(
|
|
1856
|
-
)) {
|
|
1857
|
-
throw new Error(
|
|
1858
|
-
`Invalid chat JSONL row ${index + 1}: expected an optional system message, one user message, and one assistant answer`
|
|
1859
|
-
);
|
|
2365
|
+
const expectedSeq = previous ? (previous.seq ?? -1) + 1 : 0;
|
|
2366
|
+
if (chained.seq !== expectedSeq) {
|
|
2367
|
+
return result("broken", { index, seq: chained.seq ?? null, reason: `Expected sequence ${expectedSeq}; an event was removed, reordered or inserted.` });
|
|
1860
2368
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
if (offset === 1 && row.messages[0]?.role !== "system" || row.messages[offset]?.role !== "user" || !row.messages[offset]?.content.trim() || row.messages[offset + 1]?.role !== "assistant" || !row.messages[offset + 1]?.content.trim()) {
|
|
1864
|
-
throw new Error(
|
|
1865
|
-
`Invalid chat JSONL row ${index + 1}: expected an optional system message, one non-empty user message, and one non-empty assistant answer`
|
|
1866
|
-
);
|
|
2369
|
+
if ((chained.prev_hash ?? null) !== (previous?.hash ?? null)) {
|
|
2370
|
+
return result("broken", { index, seq: chained.seq, reason: "The previous-event hash does not match; an earlier event was changed or removed." });
|
|
1867
2371
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
2372
|
+
if (eventHash(chained) !== chained.hash) {
|
|
2373
|
+
return result("broken", { index, seq: chained.seq, reason: "The event's contents do not match its hash; it was edited." });
|
|
2374
|
+
}
|
|
2375
|
+
previous = chained;
|
|
1870
2376
|
}
|
|
1871
|
-
if (
|
|
1872
|
-
return
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
};
|
|
2377
|
+
if (events.length === 0) return result("empty");
|
|
2378
|
+
if (!previous) return result("legacy");
|
|
2379
|
+
if (unchainedAfter > 0) return result("mixed");
|
|
2380
|
+
return result(legacy > 0 ? "legacy" : "verified");
|
|
1876
2381
|
}
|
|
1877
|
-
async function
|
|
1878
|
-
|
|
2382
|
+
async function sha256File2(path) {
|
|
2383
|
+
const hash = createHash3("sha256");
|
|
2384
|
+
for await (const chunk of createReadStream2(path)) hash.update(chunk);
|
|
2385
|
+
return hash.digest("hex");
|
|
1879
2386
|
}
|
|
1880
|
-
function
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
parts.push(`Guidelines:
|
|
1887
|
-
${guidelines.map((guideline) => `- ${guideline}`).join("\n")}`);
|
|
1888
|
-
}
|
|
1889
|
-
if (constraints.length > 0) {
|
|
1890
|
-
parts.push(`Constraints:
|
|
1891
|
-
${constraints.map((constraint) => `- ${constraint}`).join("\n")}`);
|
|
2387
|
+
function processAlive(pid) {
|
|
2388
|
+
try {
|
|
2389
|
+
process.kill(pid, 0);
|
|
2390
|
+
return true;
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
return error.code === "EPERM";
|
|
1892
2393
|
}
|
|
1893
|
-
return parts.join("\n\n") || "Follow the demonstrated behavior.";
|
|
1894
|
-
}
|
|
1895
|
-
function exampleToChatRow(spec, example) {
|
|
1896
|
-
const system = buildSystemMessage(spec);
|
|
1897
|
-
return {
|
|
1898
|
-
messages: [
|
|
1899
|
-
{ role: "system", content: system },
|
|
1900
|
-
{ role: "user", content: example.input },
|
|
1901
|
-
{ role: "assistant", content: example.output }
|
|
1902
|
-
]
|
|
1903
|
-
};
|
|
1904
2394
|
}
|
|
1905
|
-
function
|
|
1906
|
-
|
|
2395
|
+
async function readLockOwner(lockPath) {
|
|
2396
|
+
try {
|
|
2397
|
+
return JSON.parse(await readFile4(join3(lockPath, "owner.json"), "utf8"));
|
|
2398
|
+
} catch {
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
1907
2401
|
}
|
|
1908
|
-
function
|
|
1909
|
-
|
|
2402
|
+
async function abandonedLockOwner(lockPath, orphanMs, remoteMs) {
|
|
2403
|
+
const metadata = await stat3(lockPath).catch(() => null);
|
|
2404
|
+
if (!metadata) return null;
|
|
2405
|
+
const owner = await readLockOwner(lockPath);
|
|
2406
|
+
const age = Date.now() - metadata.mtimeMs;
|
|
2407
|
+
if (!owner) return age > orphanMs ? "ownerless" : null;
|
|
2408
|
+
if (owner.host === hostname()) return processAlive(owner.pid) ? null : owner;
|
|
2409
|
+
return age > remoteMs ? owner : null;
|
|
1910
2410
|
}
|
|
1911
|
-
async function
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
input: user.content,
|
|
1919
|
-
output: assistant.content
|
|
1920
|
-
});
|
|
2411
|
+
async function breakAbandonedLock(lockPath, abandoned) {
|
|
2412
|
+
const moved = `${lockPath}.abandoned-${randomUUID3()}`;
|
|
2413
|
+
try {
|
|
2414
|
+
await rename2(lockPath, moved);
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
if (error.code === "ENOENT") return;
|
|
2417
|
+
throw error;
|
|
1921
2418
|
}
|
|
1922
|
-
|
|
2419
|
+
const owner = await readLockOwner(moved);
|
|
2420
|
+
const expected = abandoned === "ownerless" ? void 0 : abandoned.token;
|
|
2421
|
+
if (owner?.token !== expected) {
|
|
2422
|
+
await rename2(moved, lockPath).catch(() => void 0);
|
|
2423
|
+
}
|
|
2424
|
+
await rm(moved, { recursive: true, force: true });
|
|
1923
2425
|
}
|
|
1924
|
-
async function
|
|
1925
|
-
const
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2426
|
+
async function withAppendLock(lockPath, action, options = {}) {
|
|
2427
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
2428
|
+
const token = randomUUID3();
|
|
2429
|
+
const deadline = Date.now() + timeoutMs;
|
|
2430
|
+
for (; ; ) {
|
|
2431
|
+
try {
|
|
2432
|
+
await mkdir3(lockPath, { mode: 448 });
|
|
2433
|
+
await writeFile3(join3(lockPath, "owner.json"), JSON.stringify({ token, pid: process.pid, host: hostname() }), { mode: 384 });
|
|
2434
|
+
break;
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
if (error.code !== "EEXIST") throw error;
|
|
2437
|
+
const abandoned = await abandonedLockOwner(lockPath, options.orphanMs ?? 5e3, options.remoteMs ?? 6e5);
|
|
2438
|
+
if (abandoned) {
|
|
2439
|
+
await breakAbandonedLock(lockPath, abandoned);
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if (Date.now() > deadline) throw new Error(`Timed out waiting for the run event log lock: ${lockPath}`);
|
|
2443
|
+
await new Promise((resolve17) => setTimeout(resolve17, 20));
|
|
1934
2444
|
}
|
|
1935
|
-
inputIdentities.add(identity);
|
|
1936
|
-
}
|
|
1937
|
-
const systems = new Set(
|
|
1938
|
-
normalized.rows.map((row) => row.messages.find((message) => message.role === "system")?.content).filter((value) => value !== void 0)
|
|
1939
|
-
);
|
|
1940
|
-
if (systems.size > 1) {
|
|
1941
|
-
throw new Error("General regression rows must use one consistent system message.");
|
|
1942
2445
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
);
|
|
2446
|
+
try {
|
|
2447
|
+
return await action();
|
|
2448
|
+
} finally {
|
|
2449
|
+
if ((await readLockOwner(lockPath))?.token === token) await rm(lockPath, { recursive: true, force: true });
|
|
1948
2450
|
}
|
|
1949
|
-
return {
|
|
1950
|
-
examples: normalized.rows.map((row) => ({
|
|
1951
|
-
input: row.messages.at(-2).content,
|
|
1952
|
-
output: row.messages.at(-1).content
|
|
1953
|
-
})),
|
|
1954
|
-
system: configuredSystem ?? embeddedSystem ?? "You are a helpful assistant."
|
|
1955
|
-
};
|
|
1956
2451
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
function reportInBackground(action) {
|
|
2452
|
+
async function readJsonlTail(path, accept = () => true) {
|
|
2453
|
+
let handle;
|
|
1960
2454
|
try {
|
|
1961
|
-
|
|
1962
|
-
} catch {
|
|
2455
|
+
handle = await open2(path, "r");
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
if (error.code === "ENOENT") return { last: void 0, partial: false };
|
|
2458
|
+
throw error;
|
|
2459
|
+
}
|
|
2460
|
+
try {
|
|
2461
|
+
const size = (await handle.stat()).size;
|
|
2462
|
+
if (size === 0) return { last: void 0, partial: false };
|
|
2463
|
+
let window = Math.min(size, 64 * 1024);
|
|
2464
|
+
for (; ; ) {
|
|
2465
|
+
const buffer = Buffer.alloc(window);
|
|
2466
|
+
await handle.read(buffer, 0, window, size - window);
|
|
2467
|
+
const text = buffer.toString("utf8");
|
|
2468
|
+
const partial = !text.endsWith("\n");
|
|
2469
|
+
const lines = text.split("\n");
|
|
2470
|
+
const complete = window === size ? lines : lines.slice(1);
|
|
2471
|
+
for (const line of complete.reverse()) {
|
|
2472
|
+
if (!line.trim()) continue;
|
|
2473
|
+
let row;
|
|
2474
|
+
try {
|
|
2475
|
+
row = JSON.parse(line);
|
|
2476
|
+
} catch {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
|
|
2480
|
+
if (accept(row)) return { last: row, partial };
|
|
2481
|
+
}
|
|
2482
|
+
if (window === size) return { last: void 0, partial };
|
|
2483
|
+
window = Math.min(size, window * 4);
|
|
2484
|
+
}
|
|
2485
|
+
} finally {
|
|
2486
|
+
await handle.close();
|
|
1963
2487
|
}
|
|
1964
2488
|
}
|
|
1965
|
-
function
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2489
|
+
function parseJsonlTolerant(text) {
|
|
2490
|
+
const rows = [];
|
|
2491
|
+
let unreadable = 0;
|
|
2492
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2493
|
+
if (!line.trim()) continue;
|
|
2494
|
+
try {
|
|
2495
|
+
const row = JSON.parse(line);
|
|
2496
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) {
|
|
2497
|
+
unreadable += 1;
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
rows.push(row);
|
|
2501
|
+
} catch {
|
|
2502
|
+
unreadable += 1;
|
|
1977
2503
|
}
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
if (buffered.trim()) onLine(sanitizeLogLine(buffered));
|
|
1981
|
-
});
|
|
2504
|
+
}
|
|
2505
|
+
return { rows, unreadable };
|
|
1982
2506
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2507
|
+
function buildTimeline(events) {
|
|
2508
|
+
const timeline = [];
|
|
2509
|
+
for (const event of events) {
|
|
2510
|
+
const last2 = timeline.at(-1);
|
|
2511
|
+
if (last2 && last2.stage === event.stage) {
|
|
2512
|
+
last2.status = event.status;
|
|
2513
|
+
last2.message = event.message;
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
if (last2) {
|
|
2517
|
+
last2.ended_at = event.occurred_at;
|
|
2518
|
+
last2.duration_seconds = Math.max(0, (Date.parse(event.occurred_at) - Date.parse(last2.started_at)) / 1e3);
|
|
2519
|
+
}
|
|
2520
|
+
timeline.push({
|
|
2521
|
+
stage: event.stage,
|
|
2522
|
+
status: event.status,
|
|
2523
|
+
message: event.message,
|
|
2524
|
+
started_at: event.occurred_at,
|
|
2525
|
+
ended_at: null,
|
|
2526
|
+
duration_seconds: null
|
|
2527
|
+
});
|
|
1999
2528
|
}
|
|
2000
|
-
|
|
2529
|
+
const last = timeline.at(-1);
|
|
2530
|
+
if (last && ["completed", "failed", "cancelled", "stage_completed"].includes(last.stage)) {
|
|
2531
|
+
last.ended_at = last.started_at;
|
|
2532
|
+
last.duration_seconds = 0;
|
|
2533
|
+
}
|
|
2534
|
+
return timeline;
|
|
2535
|
+
}
|
|
2536
|
+
async function auditRun(store, runId) {
|
|
2537
|
+
const state = await store.getRun(runId);
|
|
2538
|
+
const log = await store.getRunEventLog(state.id);
|
|
2539
|
+
const events = log.events;
|
|
2540
|
+
const chain = verifyEventChain(events);
|
|
2541
|
+
const completion = [...events].reverse().find((event) => event.stage === "completed");
|
|
2542
|
+
const recorded = typeof completion?.details?.report_sha256 === "string" ? completion.details.report_sha256 : null;
|
|
2543
|
+
const recordedManifest = typeof completion?.details?.artifact_manifest_sha256 === "string" ? completion.details.artifact_manifest_sha256 : null;
|
|
2544
|
+
const files = await Promise.all((await store.getRunReportPaths(state.id)).map(async (path) => {
|
|
2545
|
+
const sha256 = await sha256File2(path);
|
|
2546
|
+
return { path, sha256, matches_recorded: recorded ? sha256 === recorded : null };
|
|
2547
|
+
}));
|
|
2548
|
+
const manifestPath = state.artifact_dir ? join3(state.artifact_dir, "artifact-manifest.json") : null;
|
|
2549
|
+
const manifestFile = manifestPath && (await stat3(manifestPath).catch(() => null))?.isFile() ? await sha256File2(manifestPath).then((sha256) => ({
|
|
2550
|
+
path: manifestPath,
|
|
2551
|
+
sha256,
|
|
2552
|
+
matches_recorded: recordedManifest ? sha256 === recordedManifest : null
|
|
2553
|
+
})) : null;
|
|
2554
|
+
const report = await store.getRunReport(state.id).catch(() => null);
|
|
2555
|
+
const findings = [];
|
|
2556
|
+
let verdict = "verified";
|
|
2557
|
+
const tampered = (finding) => {
|
|
2558
|
+
findings.push(finding);
|
|
2559
|
+
verdict = "tampered";
|
|
2560
|
+
};
|
|
2561
|
+
const incomplete = (finding) => {
|
|
2562
|
+
findings.push(finding);
|
|
2563
|
+
if (verdict === "verified") verdict = "incomplete";
|
|
2564
|
+
};
|
|
2565
|
+
const completed = state.status === "completed";
|
|
2566
|
+
if (chain.status === "broken") {
|
|
2567
|
+
tampered(`Event log chain is broken at event ${chain.break.index}: ${chain.break.reason}`);
|
|
2568
|
+
} else if (chain.status === "empty") {
|
|
2569
|
+
if (completed) tampered("The run is completed but its event log is missing or empty.");
|
|
2570
|
+
else incomplete("The run has no events.");
|
|
2571
|
+
} else if (chain.status === "legacy") {
|
|
2572
|
+
incomplete(`${chain.legacy_events} event(s) were recorded before event chaining (older TT version) and cannot be verified.`);
|
|
2573
|
+
} else if (chain.status === "mixed") {
|
|
2574
|
+
incomplete(`${chain.unchained_after_chain} event(s) were written without hashes after chaining began, e.g. by an older TT process.`);
|
|
2575
|
+
}
|
|
2576
|
+
if (log.unreadable_lines > 0) incomplete(`${log.unreadable_lines} event line(s) are unreadable, likely from an interrupted write.`);
|
|
2577
|
+
for (const file of files) {
|
|
2578
|
+
if (file.matches_recorded === false) {
|
|
2579
|
+
tampered(`Report ${file.path} changed after the run completed (sha256 ${file.sha256.slice(0, 12)}, recorded ${recorded.slice(0, 12)}).`);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
if (manifestFile?.matches_recorded === false) {
|
|
2583
|
+
tampered(`Artifact manifest ${manifestFile.path} changed after the run completed.`);
|
|
2584
|
+
}
|
|
2585
|
+
if (!completed) {
|
|
2586
|
+
incomplete(`The run is ${state.status}; only completed runs have recorded report digests.`);
|
|
2587
|
+
} else if (!recorded) {
|
|
2588
|
+
if (chain.status === "verified") tampered("The completion event is missing from a chained event log; the log was truncated.");
|
|
2589
|
+
else if (chain.status !== "broken" && chain.status !== "empty") incomplete("No report digest was recorded at completion (older TT version).");
|
|
2590
|
+
} else if (files.length === 0) {
|
|
2591
|
+
incomplete("The recorded report file is missing.");
|
|
2592
|
+
}
|
|
2593
|
+
if (completed && recordedManifest && !manifestFile) incomplete("The artifact manifest recorded at completion is missing.");
|
|
2594
|
+
if (completed && report && !report.provenance) incomplete("The report has no provenance block (older TT version).");
|
|
2595
|
+
return {
|
|
2596
|
+
run_id: state.id,
|
|
2597
|
+
status: state.status,
|
|
2598
|
+
verdict,
|
|
2599
|
+
findings,
|
|
2600
|
+
chain: { ...chain, unreadable_lines: log.unreadable_lines },
|
|
2601
|
+
report: { recorded_sha256: recorded, files },
|
|
2602
|
+
artifact_manifest: { recorded_sha256: recordedManifest, file: manifestFile },
|
|
2603
|
+
timeline: buildTimeline(events),
|
|
2604
|
+
provenance: report?.provenance ?? null
|
|
2605
|
+
};
|
|
2001
2606
|
}
|
|
2002
2607
|
|
|
2003
2608
|
// src/local-runtime/store.ts
|
|
2004
|
-
import { appendFile, chmod, copyFile as copyFile2, lstat as lstat3, mkdir as
|
|
2005
|
-
import { dirname as dirname4, join as
|
|
2006
|
-
import { randomUUID as
|
|
2609
|
+
import { appendFile, chmod, copyFile as copyFile2, lstat as lstat3, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, rm as rm2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
|
|
2610
|
+
import { dirname as dirname4, join as join4, resolve as resolve6, sep as sep3 } from "path";
|
|
2611
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2007
2612
|
function isTerminalRunState(state) {
|
|
2008
2613
|
return state.status === "stage_completed" || state.status === "completed" || state.status === "failed" || state.status === "cancelled" && state.current_stage === "cancelled" && Boolean(state.completed_at);
|
|
2009
2614
|
}
|
|
@@ -2013,22 +2618,22 @@ function defaultLocalHome() {
|
|
|
2013
2618
|
function localStorePaths(root) {
|
|
2014
2619
|
return {
|
|
2015
2620
|
root,
|
|
2016
|
-
specsDir:
|
|
2017
|
-
runsDir:
|
|
2018
|
-
modelsDir:
|
|
2019
|
-
datasetsDir:
|
|
2621
|
+
specsDir: join4(root, "specs"),
|
|
2622
|
+
runsDir: join4(root, "runs"),
|
|
2623
|
+
modelsDir: join4(root, "models"),
|
|
2624
|
+
datasetsDir: join4(root, "datasets")
|
|
2020
2625
|
};
|
|
2021
2626
|
}
|
|
2022
2627
|
async function exists2(path) {
|
|
2023
2628
|
try {
|
|
2024
|
-
await
|
|
2629
|
+
await stat4(path);
|
|
2025
2630
|
return true;
|
|
2026
2631
|
} catch {
|
|
2027
2632
|
return false;
|
|
2028
2633
|
}
|
|
2029
2634
|
}
|
|
2030
2635
|
async function ensurePrivateDirectory(path) {
|
|
2031
|
-
await
|
|
2636
|
+
await mkdir4(path, { recursive: true, mode: 448 });
|
|
2032
2637
|
const metadata = await lstat3(path);
|
|
2033
2638
|
if (metadata.isSymbolicLink()) {
|
|
2034
2639
|
throw new Error(`Refusing symbolic link in local store: ${path}`);
|
|
@@ -2046,7 +2651,7 @@ async function hardenPrivateTree(path) {
|
|
|
2046
2651
|
if (metadata.isDirectory()) {
|
|
2047
2652
|
await chmod(path, 448);
|
|
2048
2653
|
for (const entry of await readdir3(path)) {
|
|
2049
|
-
await hardenPrivateTree(
|
|
2654
|
+
await hardenPrivateTree(join4(path, entry));
|
|
2050
2655
|
}
|
|
2051
2656
|
return;
|
|
2052
2657
|
}
|
|
@@ -2076,17 +2681,17 @@ async function hardenExistingLocalStore(root) {
|
|
|
2076
2681
|
}
|
|
2077
2682
|
async function writePrivateJsonAtomic(path, value) {
|
|
2078
2683
|
await ensurePrivateDirectory(dirname4(path));
|
|
2079
|
-
const tmp = `${path}.${process.pid}.${Date.now()}.${
|
|
2080
|
-
await
|
|
2684
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.${randomUUID4()}.tmp`;
|
|
2685
|
+
await writeFile4(tmp, `${JSON.stringify(value, null, 2)}
|
|
2081
2686
|
`, {
|
|
2082
2687
|
encoding: "utf8",
|
|
2083
2688
|
mode: 384
|
|
2084
2689
|
});
|
|
2085
|
-
await
|
|
2690
|
+
await rename3(tmp, path);
|
|
2086
2691
|
await chmod(path, 384);
|
|
2087
2692
|
}
|
|
2088
2693
|
async function readJson2(path) {
|
|
2089
|
-
return JSON.parse(await
|
|
2694
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
2090
2695
|
}
|
|
2091
2696
|
async function appendJsonl(path, value) {
|
|
2092
2697
|
await ensurePrivateDirectory(dirname4(path));
|
|
@@ -2099,13 +2704,9 @@ async function appendJsonl(path, value) {
|
|
|
2099
2704
|
}
|
|
2100
2705
|
async function writePrivateText(path, value) {
|
|
2101
2706
|
await ensurePrivateDirectory(dirname4(path));
|
|
2102
|
-
await
|
|
2707
|
+
await writeFile4(path, value, { encoding: "utf8", mode: 384 });
|
|
2103
2708
|
await chmod(path, 384);
|
|
2104
2709
|
}
|
|
2105
|
-
async function readJsonl(path) {
|
|
2106
|
-
if (!await exists2(path)) return [];
|
|
2107
|
-
return (await readFile4(path, "utf8")).split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
2108
|
-
}
|
|
2109
2710
|
async function copyIfExists(from, to) {
|
|
2110
2711
|
if (!await exists2(from)) return;
|
|
2111
2712
|
await ensurePrivateDirectory(dirname4(to));
|
|
@@ -2144,15 +2745,15 @@ function isPathInside(root, candidate) {
|
|
|
2144
2745
|
function createLocalStore(root = defaultLocalHome()) {
|
|
2145
2746
|
const resolvedRoot = resolve6(root);
|
|
2146
2747
|
const paths = localStorePaths(resolvedRoot);
|
|
2147
|
-
const runDir = (id) =>
|
|
2148
|
-
const runStatePath = (id) =>
|
|
2149
|
-
const runEventsPath = (id) =>
|
|
2150
|
-
const runRequestPath = (id) =>
|
|
2151
|
-
const runReportPath = (id) =>
|
|
2152
|
-
const cancellationPath = (id) =>
|
|
2153
|
-
const specDir = (id) =>
|
|
2154
|
-
const specPath = (id) =>
|
|
2155
|
-
const modelPath = (id) =>
|
|
2748
|
+
const runDir = (id) => join4(paths.runsDir, id);
|
|
2749
|
+
const runStatePath = (id) => join4(runDir(id), "state.json");
|
|
2750
|
+
const runEventsPath = (id) => join4(runDir(id), "progress.jsonl");
|
|
2751
|
+
const runRequestPath = (id) => join4(runDir(id), "request.json");
|
|
2752
|
+
const runReportPath = (id) => join4(runDir(id), "run-report.json");
|
|
2753
|
+
const cancellationPath = (id) => join4(runDir(id), "cancel.requested");
|
|
2754
|
+
const specDir = (id) => join4(paths.specsDir, id);
|
|
2755
|
+
const specPath = (id) => join4(specDir(id), "spec.json");
|
|
2756
|
+
const modelPath = (id) => join4(paths.modelsDir, id, "model.json");
|
|
2156
2757
|
let ensurePromise;
|
|
2157
2758
|
async function ensureOnce() {
|
|
2158
2759
|
await ensurePrivateDirectory(paths.root);
|
|
@@ -2180,8 +2781,8 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2180
2781
|
if (!await exists2(path)) return null;
|
|
2181
2782
|
const [spec, fileStats, directoryStats] = await Promise.all([
|
|
2182
2783
|
readJson2(path),
|
|
2183
|
-
|
|
2184
|
-
|
|
2784
|
+
stat4(path),
|
|
2785
|
+
stat4(specDir(id))
|
|
2185
2786
|
]);
|
|
2186
2787
|
const createdAt = directoryStats.birthtimeMs > 0 ? directoryStats.birthtime : directoryStats.ctime;
|
|
2187
2788
|
return {
|
|
@@ -2228,15 +2829,30 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2228
2829
|
if (!record) throw new Error(`Failed to persist spec: ${specId}`);
|
|
2229
2830
|
return record;
|
|
2230
2831
|
}
|
|
2832
|
+
async function readRunEventLog(id) {
|
|
2833
|
+
const state = await getRun(id);
|
|
2834
|
+
const path = runEventsPath(state.id);
|
|
2835
|
+
if (!await exists2(path)) return { events: [], unreadable_lines: 0 };
|
|
2836
|
+
const parsed = parseJsonlTolerant(await readFile5(path, "utf8"));
|
|
2837
|
+
return { events: parsed.rows, unreadable_lines: parsed.unreadable };
|
|
2838
|
+
}
|
|
2231
2839
|
async function appendRunEvent(state, event) {
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2840
|
+
const path = runEventsPath(state.id);
|
|
2841
|
+
await ensurePrivateDirectory(dirname4(path));
|
|
2842
|
+
const row = await withAppendLock(`${path}.lock`, async () => {
|
|
2843
|
+
const { partial } = await readJsonlTail(path);
|
|
2844
|
+
const { last: previous } = await readJsonlTail(path, (row2) => typeof row2.hash === "string");
|
|
2845
|
+
if (partial) await appendFile(path, "\n", { encoding: "utf8", mode: 384 });
|
|
2846
|
+
const chained = chainEvent({
|
|
2847
|
+
id: randomUUID4(),
|
|
2848
|
+
run_id: state.id,
|
|
2849
|
+
occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2850
|
+
...event
|
|
2851
|
+
}, previous);
|
|
2852
|
+
await appendJsonl(path, chained);
|
|
2853
|
+
return chained;
|
|
2854
|
+
});
|
|
2855
|
+
await appendJsonl(join4(state.artifact_dir, "progress.jsonl"), {
|
|
2240
2856
|
at: row.occurred_at,
|
|
2241
2857
|
stage: row.stage,
|
|
2242
2858
|
status: row.status,
|
|
@@ -2267,8 +2883,8 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2267
2883
|
const previous = await getRun(runId);
|
|
2268
2884
|
if (isTerminalRunState(previous)) return previous;
|
|
2269
2885
|
await Promise.all([
|
|
2270
|
-
|
|
2271
|
-
|
|
2886
|
+
rm2(join4(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true }),
|
|
2887
|
+
rm2(runReportPath(previous.id), { force: true })
|
|
2272
2888
|
]);
|
|
2273
2889
|
if (previous.status === "cancelled" && previous.current_stage === "cancel_requested") return previous;
|
|
2274
2890
|
const requested = {
|
|
@@ -2434,7 +3050,7 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2434
3050
|
createdAt: completedAt
|
|
2435
3051
|
});
|
|
2436
3052
|
if (!model) {
|
|
2437
|
-
await
|
|
3053
|
+
await rm2(join4(paths.modelsDir, `local-${report.run_id}`), { recursive: true, force: true });
|
|
2438
3054
|
}
|
|
2439
3055
|
const previous = await getRun(report.run_id);
|
|
2440
3056
|
const state = {
|
|
@@ -2455,20 +3071,27 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2455
3071
|
if (await exists2(cancellationPath(report.run_id))) {
|
|
2456
3072
|
return preserveCancellationRequestState(report.run_id);
|
|
2457
3073
|
}
|
|
3074
|
+
const manifestPath = join4(artifactDir, "artifact-manifest.json");
|
|
2458
3075
|
await appendRunEvent(state, {
|
|
2459
3076
|
stage: "completed",
|
|
2460
3077
|
status: "completed",
|
|
2461
3078
|
message: "Run completed successfully.",
|
|
2462
|
-
details: {
|
|
3079
|
+
details: {
|
|
3080
|
+
report_path: reportPath,
|
|
3081
|
+
// Pin the published evidence: `tt runs audit` re-hashes these files.
|
|
3082
|
+
report_sha256: await sha256File2(runReportPath(report.run_id)),
|
|
3083
|
+
...await exists2(manifestPath) ? { artifact_manifest_sha256: await sha256File2(manifestPath) } : {},
|
|
3084
|
+
...model ? { model_id: model.id } : {}
|
|
3085
|
+
}
|
|
2463
3086
|
});
|
|
2464
3087
|
return state;
|
|
2465
3088
|
},
|
|
2466
3089
|
registerModel: registerModelRecord,
|
|
2467
3090
|
async invalidateRunOutputs(runId, options) {
|
|
2468
3091
|
const previous = await getRun(runId);
|
|
2469
|
-
if (options.report) await
|
|
3092
|
+
if (options.report) await rm2(runReportPath(previous.id), { force: true });
|
|
2470
3093
|
if (options.model) {
|
|
2471
|
-
await
|
|
3094
|
+
await rm2(join4(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true });
|
|
2472
3095
|
}
|
|
2473
3096
|
const state = {
|
|
2474
3097
|
...previous,
|
|
@@ -2511,7 +3134,7 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2511
3134
|
`);
|
|
2512
3135
|
const latest = await getRun(state.id);
|
|
2513
3136
|
if (isTerminalRunState(latest)) {
|
|
2514
|
-
if (latest.status !== "cancelled") await
|
|
3137
|
+
if (latest.status !== "cancelled") await rm2(cancellationPath(state.id), { force: true });
|
|
2515
3138
|
return;
|
|
2516
3139
|
}
|
|
2517
3140
|
const updated = {
|
|
@@ -2538,8 +3161,23 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2538
3161
|
},
|
|
2539
3162
|
getRun,
|
|
2540
3163
|
async getRunEvents(id) {
|
|
3164
|
+
return (await readRunEventLog(id)).events;
|
|
3165
|
+
},
|
|
3166
|
+
getRunEventLog: readRunEventLog,
|
|
3167
|
+
async getRunReportPaths(id) {
|
|
2541
3168
|
const state = await getRun(id);
|
|
2542
|
-
|
|
3169
|
+
const candidates = [];
|
|
3170
|
+
if (state.report_path) {
|
|
3171
|
+
const reportPath = resolve6(state.report_path);
|
|
3172
|
+
const allowedRoots = [resolvedRoot, ...state.artifact_dir ? [resolve6(state.artifact_dir)] : []];
|
|
3173
|
+
if (allowedRoots.some((allowed) => isPathInside(allowed, reportPath))) candidates.push(reportPath);
|
|
3174
|
+
}
|
|
3175
|
+
candidates.push(runReportPath(state.id));
|
|
3176
|
+
const existing = [];
|
|
3177
|
+
for (const path of new Set(candidates)) {
|
|
3178
|
+
if ((await lstat3(path).catch(() => null))?.isFile()) existing.push(path);
|
|
3179
|
+
}
|
|
3180
|
+
return existing;
|
|
2543
3181
|
},
|
|
2544
3182
|
async getRunReport(id) {
|
|
2545
3183
|
const state = await getRun(id);
|
|
@@ -2570,38 +3208,216 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2570
3208
|
}
|
|
2571
3209
|
|
|
2572
3210
|
// src/local-runtime/prefetch.ts
|
|
2573
|
-
import { createHash as
|
|
2574
|
-
import { createReadStream as
|
|
2575
|
-
import { lstat as lstat4, mkdir as
|
|
2576
|
-
import { isAbsolute as isAbsolute4, join as
|
|
3211
|
+
import { createHash as createHash5 } from "crypto";
|
|
3212
|
+
import { createReadStream as createReadStream3 } from "fs";
|
|
3213
|
+
import { lstat as lstat4, mkdir as mkdir6, readFile as readFile6, readdir as readdir4, realpath as realpath2, stat as stat5 } from "fs/promises";
|
|
3214
|
+
import { isAbsolute as isAbsolute4, join as join7, relative as relative3, resolve as resolve9, sep as sep4 } from "path";
|
|
2577
3215
|
|
|
2578
3216
|
// src/local-runtime/process-runner.ts
|
|
2579
|
-
import { createHash as
|
|
3217
|
+
import { createHash as createHash4 } from "crypto";
|
|
2580
3218
|
import { spawn } from "child_process";
|
|
2581
3219
|
import { constants, readFileSync } from "fs";
|
|
2582
|
-
import { mkdir as
|
|
2583
|
-
import { dirname as dirname5, join as
|
|
3220
|
+
import { mkdir as mkdir5, open as open3 } from "fs/promises";
|
|
3221
|
+
import { dirname as dirname5, join as join5, resolve as resolve7 } from "path";
|
|
3222
|
+
|
|
3223
|
+
// src/local-runtime/progress-bar.ts
|
|
3224
|
+
var PROGRESS_PREFIX = "@@tt-progress ";
|
|
3225
|
+
function nonNegativeInteger(value) {
|
|
3226
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
|
3227
|
+
}
|
|
3228
|
+
function parseProgressLine(line) {
|
|
3229
|
+
const start = line.indexOf(PROGRESS_PREFIX);
|
|
3230
|
+
if (start !== 0) return null;
|
|
3231
|
+
let value;
|
|
3232
|
+
try {
|
|
3233
|
+
value = JSON.parse(line.slice(PROGRESS_PREFIX.length));
|
|
3234
|
+
} catch {
|
|
3235
|
+
return null;
|
|
3236
|
+
}
|
|
3237
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3238
|
+
const record = value;
|
|
3239
|
+
const completed = nonNegativeInteger(record.completed_bytes);
|
|
3240
|
+
const total = nonNegativeInteger(record.total_bytes);
|
|
3241
|
+
if (completed === null || total === null) return null;
|
|
3242
|
+
return {
|
|
3243
|
+
label: typeof record.label === "string" ? record.label : "download",
|
|
3244
|
+
phase: typeof record.phase === "string" ? record.phase : "download",
|
|
3245
|
+
completed_bytes: completed,
|
|
3246
|
+
total_bytes: total,
|
|
3247
|
+
files_completed: nonNegativeInteger(record.files_completed) ?? 0,
|
|
3248
|
+
files_total: nonNegativeInteger(record.files_total) ?? 0
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
function formatBytes(bytes) {
|
|
3252
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
3253
|
+
let value = bytes;
|
|
3254
|
+
let unit = 0;
|
|
3255
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
3256
|
+
value /= 1024;
|
|
3257
|
+
unit += 1;
|
|
3258
|
+
}
|
|
3259
|
+
return `${unit === 0 ? value.toFixed(0) : value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unit]}`;
|
|
3260
|
+
}
|
|
3261
|
+
function formatDuration(seconds) {
|
|
3262
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "--";
|
|
3263
|
+
const whole = Math.round(seconds);
|
|
3264
|
+
const hours = Math.floor(whole / 3600);
|
|
3265
|
+
const minutes = Math.floor(whole % 3600 / 60);
|
|
3266
|
+
const rest = whole % 60;
|
|
3267
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
3268
|
+
if (minutes > 0) return `${minutes}m ${String(rest).padStart(2, "0")}s`;
|
|
3269
|
+
return `${rest}s`;
|
|
3270
|
+
}
|
|
3271
|
+
var RateTracker = class {
|
|
3272
|
+
constructor(windowMs = 15e3) {
|
|
3273
|
+
this.windowMs = windowMs;
|
|
3274
|
+
}
|
|
3275
|
+
samples = [];
|
|
3276
|
+
add(at, bytes) {
|
|
3277
|
+
const last = this.samples.at(-1);
|
|
3278
|
+
if (last && bytes < last.bytes) this.samples = [];
|
|
3279
|
+
this.samples.push({ at, bytes });
|
|
3280
|
+
while (this.samples.length > 2 && at - this.samples[0].at > this.windowMs) this.samples.shift();
|
|
3281
|
+
}
|
|
3282
|
+
bytesPerSecond() {
|
|
3283
|
+
const first = this.samples[0];
|
|
3284
|
+
const last = this.samples.at(-1);
|
|
3285
|
+
if (!first || !last || last.at - first.at < 500) return null;
|
|
3286
|
+
return (last.bytes - first.bytes) / ((last.at - first.at) / 1e3);
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
function formatProgressLine(progress, rate, columns) {
|
|
3290
|
+
const total = progress.total_bytes;
|
|
3291
|
+
const fraction = total > 0 ? Math.min(1, progress.completed_bytes / total) : 1;
|
|
3292
|
+
const fields = [
|
|
3293
|
+
{ text: `${(fraction * 100).toFixed(1).padStart(5)}%` },
|
|
3294
|
+
{ text: `${formatBytes(progress.completed_bytes)} / ${formatBytes(total)}` }
|
|
3295
|
+
];
|
|
3296
|
+
const done = progress.completed_bytes >= total;
|
|
3297
|
+
if (!done && rate !== null && rate > 0) {
|
|
3298
|
+
fields.push(
|
|
3299
|
+
{ text: `${formatBytes(rate)}/s`, drop: 2 },
|
|
3300
|
+
{ text: `ETA ${formatDuration((total - progress.completed_bytes) / rate)}`, drop: 3 }
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
if (progress.files_total > 0) fields.push({ text: `${progress.files_completed}/${progress.files_total} files`, drop: 1 });
|
|
3304
|
+
const prefix = `[tt] ${progress.label}: `;
|
|
3305
|
+
const join16 = () => fields.map((field) => field.text).join(" ");
|
|
3306
|
+
if (columns === void 0) return `${prefix}${join16()}`;
|
|
3307
|
+
for (const rank of [1, 2, 3]) {
|
|
3308
|
+
if (prefix.length + join16().length <= columns - 1) break;
|
|
3309
|
+
const index = fields.findIndex((field) => field.drop === rank);
|
|
3310
|
+
if (index !== -1) fields.splice(index, 1);
|
|
3311
|
+
}
|
|
3312
|
+
const text = join16();
|
|
3313
|
+
const barWidth = Math.min(30, columns - 1 - prefix.length - text.length - 3);
|
|
3314
|
+
if (barWidth < 10) return `${prefix}${text}`.slice(0, Math.max(0, columns - 1));
|
|
3315
|
+
const filled = Math.round(fraction * barWidth);
|
|
3316
|
+
return `${prefix}[${"#".repeat(filled)}${"-".repeat(barWidth - filled)}] ${text}`;
|
|
3317
|
+
}
|
|
3318
|
+
function createProgressRenderer(options) {
|
|
3319
|
+
const now = options.now ?? (() => Date.now());
|
|
3320
|
+
const rates = /* @__PURE__ */ new Map();
|
|
3321
|
+
const lastLine = /* @__PURE__ */ new Map();
|
|
3322
|
+
const completed = /* @__PURE__ */ new Set();
|
|
3323
|
+
let drawn = false;
|
|
3324
|
+
let lastDraw = Number.NEGATIVE_INFINITY;
|
|
3325
|
+
const finished = (progress) => progress.phase === "downloaded" || progress.total_bytes > 0 && progress.completed_bytes >= progress.total_bytes;
|
|
3326
|
+
return {
|
|
3327
|
+
update(progress) {
|
|
3328
|
+
const at = now();
|
|
3329
|
+
const tracker = rates.get(progress.label) ?? new RateTracker();
|
|
3330
|
+
rates.set(progress.label, tracker);
|
|
3331
|
+
tracker.add(at, progress.completed_bytes);
|
|
3332
|
+
const done = finished(progress);
|
|
3333
|
+
if (options.tty) {
|
|
3334
|
+
if (done && completed.has(progress.label)) return;
|
|
3335
|
+
if (!done) completed.delete(progress.label);
|
|
3336
|
+
if (!done && at - lastDraw < 100) return;
|
|
3337
|
+
lastDraw = at;
|
|
3338
|
+
const line = formatProgressLine(progress, tracker.bytesPerSecond(), Math.max(40, options.columns ?? 80));
|
|
3339
|
+
options.write(`\r\x1B[2K${line}${done ? "\n" : ""}`);
|
|
3340
|
+
drawn = !done;
|
|
3341
|
+
if (done) {
|
|
3342
|
+
rates.delete(progress.label);
|
|
3343
|
+
completed.add(progress.label);
|
|
3344
|
+
}
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
const fraction = progress.total_bytes > 0 ? progress.completed_bytes / progress.total_bytes : 1;
|
|
3348
|
+
const decile = Math.floor(fraction * 10);
|
|
3349
|
+
const previous = lastLine.get(progress.label);
|
|
3350
|
+
const interval = options.lineIntervalMs ?? 3e4;
|
|
3351
|
+
if (previous && !done && decile <= previous.decile && at - previous.at < interval) return;
|
|
3352
|
+
if (previous && done && previous.decile === 10) return;
|
|
3353
|
+
lastLine.set(progress.label, { at, decile: done ? 10 : decile });
|
|
3354
|
+
options.write(`${formatProgressLine(progress, tracker.bytesPerSecond())}
|
|
3355
|
+
`);
|
|
3356
|
+
},
|
|
3357
|
+
interrupt() {
|
|
3358
|
+
if (!drawn) return;
|
|
3359
|
+
options.write("\r\x1B[2K");
|
|
3360
|
+
drawn = false;
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
function progressTty(env = process.env, stream = process.stderr) {
|
|
3365
|
+
if (env.TT_PROGRESS_TTY === "0") return false;
|
|
3366
|
+
return env.TT_PROGRESS_TTY === "1" || Boolean(stream.isTTY);
|
|
3367
|
+
}
|
|
3368
|
+
function progressColumns(env = process.env, stream = process.stderr) {
|
|
3369
|
+
const hinted = Number(env.TT_PROGRESS_COLUMNS);
|
|
3370
|
+
if (Number.isInteger(hinted) && hinted > 0) return hinted;
|
|
3371
|
+
return stream.columns;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// src/local-runtime/process-runner.ts
|
|
2584
3375
|
var packageRoot = localRuntimePackageRoot(import.meta.url);
|
|
2585
|
-
var bundledProject =
|
|
3376
|
+
var bundledProject = join5(packageRoot, "training/adapter");
|
|
2586
3377
|
var bundledRuntimeHash = (() => {
|
|
2587
|
-
const hash =
|
|
3378
|
+
const hash = createHash4("sha256");
|
|
2588
3379
|
for (const name of ["pyproject.toml", "uv.lock"]) {
|
|
2589
|
-
hash.update(readFileSync(
|
|
3380
|
+
hash.update(readFileSync(join5(bundledProject, name)));
|
|
2590
3381
|
}
|
|
2591
3382
|
return hash.digest("hex").slice(0, 20);
|
|
2592
3383
|
})();
|
|
2593
3384
|
var BUNDLED_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv", bundledRuntimeHash);
|
|
2594
|
-
var foundationProject =
|
|
3385
|
+
var foundationProject = join5(packageRoot, "training/foundation");
|
|
2595
3386
|
var foundationRuntimeHash = (() => {
|
|
2596
|
-
const hash =
|
|
3387
|
+
const hash = createHash4("sha256");
|
|
2597
3388
|
for (const name of ["pyproject.toml", "uv.lock"]) {
|
|
2598
|
-
hash.update(readFileSync(
|
|
3389
|
+
hash.update(readFileSync(join5(foundationProject, name)));
|
|
2599
3390
|
}
|
|
2600
3391
|
return hash.digest("hex").slice(0, 20);
|
|
2601
3392
|
})();
|
|
2602
3393
|
var FOUNDATION_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv-foundation", foundationRuntimeHash);
|
|
3394
|
+
var decisionProject = join5(packageRoot, "training/decision");
|
|
3395
|
+
var decisionRuntimeHash = (() => {
|
|
3396
|
+
const hash = createHash4("sha256");
|
|
3397
|
+
for (const name of ["pyproject.toml", "uv.lock"]) {
|
|
3398
|
+
hash.update(readFileSync(join5(decisionProject, name)));
|
|
3399
|
+
}
|
|
3400
|
+
return hash.digest("hex").slice(0, 20);
|
|
3401
|
+
})();
|
|
3402
|
+
var DECISION_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv-decision", decisionRuntimeHash);
|
|
3403
|
+
function buildDecisionPythonCommand(entrypoint, args = []) {
|
|
3404
|
+
const commandArgs = [
|
|
3405
|
+
"run",
|
|
3406
|
+
"--frozen",
|
|
3407
|
+
"--quiet",
|
|
3408
|
+
"--project",
|
|
3409
|
+
decisionProject,
|
|
3410
|
+
"python",
|
|
3411
|
+
join5(decisionProject, "src", entrypoint),
|
|
3412
|
+
...args
|
|
3413
|
+
];
|
|
3414
|
+
return { command: "uv", commandArgs, displayCommand: ["uv", ...commandArgs] };
|
|
3415
|
+
}
|
|
3416
|
+
function withDecisionPythonEnvironment(env) {
|
|
3417
|
+
return { ...env, UV_PROJECT_ENVIRONMENT: DECISION_PYTHON_ENVIRONMENT };
|
|
3418
|
+
}
|
|
2603
3419
|
function buildBundledPythonCommand(entrypoint, args = []) {
|
|
2604
|
-
const target = entrypoint === "-c" ? entrypoint :
|
|
3420
|
+
const target = entrypoint === "-c" ? entrypoint : join5(bundledProject, "src", entrypoint);
|
|
2605
3421
|
const commandArgs = [
|
|
2606
3422
|
"run",
|
|
2607
3423
|
"--frozen",
|
|
@@ -2625,7 +3441,7 @@ function withBundledPythonEnvironment(env) {
|
|
|
2625
3441
|
};
|
|
2626
3442
|
}
|
|
2627
3443
|
function buildFoundationPythonCommand(entrypoint, args = []) {
|
|
2628
|
-
const target = entrypoint === "-c" ? entrypoint :
|
|
3444
|
+
const target = entrypoint === "-c" ? entrypoint : join5(foundationProject, "src", entrypoint);
|
|
2629
3445
|
const commandArgs = [
|
|
2630
3446
|
"run",
|
|
2631
3447
|
"--frozen",
|
|
@@ -2656,7 +3472,7 @@ var ProcessCancelledError = class extends Error {
|
|
|
2656
3472
|
};
|
|
2657
3473
|
async function openProcessLog(path, exclusive, append) {
|
|
2658
3474
|
const flags = constants.O_WRONLY | constants.O_CREAT | (exclusive ? constants.O_EXCL : append ? constants.O_APPEND : constants.O_TRUNC) | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW);
|
|
2659
|
-
const handle = await
|
|
3475
|
+
const handle = await open3(path, flags, 384);
|
|
2660
3476
|
try {
|
|
2661
3477
|
return handle.createWriteStream({ autoClose: true });
|
|
2662
3478
|
} catch (error) {
|
|
@@ -2665,7 +3481,7 @@ async function openProcessLog(path, exclusive, append) {
|
|
|
2665
3481
|
}
|
|
2666
3482
|
}
|
|
2667
3483
|
async function runLoggedProcess(args) {
|
|
2668
|
-
if (args.logPath) await
|
|
3484
|
+
if (args.logPath) await mkdir5(dirname5(args.logPath), { recursive: true });
|
|
2669
3485
|
const logStream = args.logPath ? await openProcessLog(args.logPath, args.exclusiveLog ?? false, args.appendLog ?? false) : null;
|
|
2670
3486
|
let stderr = "";
|
|
2671
3487
|
try {
|
|
@@ -2739,6 +3555,11 @@ async function runLoggedProcess(args) {
|
|
|
2739
3555
|
child.stderr.pipe(logStream, { end: false });
|
|
2740
3556
|
}
|
|
2741
3557
|
forwardStreamLines(child.stdout, (line) => {
|
|
3558
|
+
const progress = parseProgressLine(line);
|
|
3559
|
+
if (progress) {
|
|
3560
|
+
reportInBackground(() => args.reporter?.onProgress?.(progress));
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
2742
3563
|
args.onLine?.(line, "stdout");
|
|
2743
3564
|
if (args.reporter?.verbose) {
|
|
2744
3565
|
reportInBackground(() => args.reporter?.onLog?.({ stage: args.stage, stream: "stdout", message: line }));
|
|
@@ -2788,7 +3609,7 @@ async function runLoggedProcess(args) {
|
|
|
2788
3609
|
|
|
2789
3610
|
// src/local-runtime/huggingface-cache.ts
|
|
2790
3611
|
import { homedir as homedir3 } from "os";
|
|
2791
|
-
import { dirname as dirname6, join as
|
|
3612
|
+
import { dirname as dirname6, join as join6, resolve as resolve8 } from "path";
|
|
2792
3613
|
function minimalMachineLearningEnvironment(env) {
|
|
2793
3614
|
const exact = /* @__PURE__ */ new Set([
|
|
2794
3615
|
"PATH",
|
|
@@ -2820,6 +3641,9 @@ function minimalMachineLearningEnvironment(env) {
|
|
|
2820
3641
|
"HF_HUB_DISABLE_TELEMETRY",
|
|
2821
3642
|
"TRANSFORMERS_OFFLINE",
|
|
2822
3643
|
"HF_HUB_ENABLE_HF_TRANSFER",
|
|
3644
|
+
"HF_HUB_DOWNLOAD_TIMEOUT",
|
|
3645
|
+
"HF_HUB_ETAG_TIMEOUT",
|
|
3646
|
+
"TT_HF_DOWNLOAD_MAX_ATTEMPTS",
|
|
2823
3647
|
"PYTHONUTF8",
|
|
2824
3648
|
"PYTHONIOENCODING"
|
|
2825
3649
|
]);
|
|
@@ -2845,12 +3669,12 @@ function resolveHuggingFaceCacheLayout(modelCache) {
|
|
|
2845
3669
|
const hfHome = resolve8(modelCache);
|
|
2846
3670
|
return {
|
|
2847
3671
|
hfHome,
|
|
2848
|
-
hubCache:
|
|
3672
|
+
hubCache: join6(hfHome, "hub")
|
|
2849
3673
|
};
|
|
2850
3674
|
}
|
|
2851
3675
|
function withHuggingFaceCacheEnvironment(env, modelCache) {
|
|
2852
3676
|
const inheritedHub = env.HF_HUB_CACHE ?? env.HUGGINGFACE_HUB_CACHE;
|
|
2853
|
-
const canonicalHome = modelCache ?? env.HF_HOME ?? (inheritedHub ? dirname6(resolve8(inheritedHub)) :
|
|
3677
|
+
const canonicalHome = modelCache ?? env.HF_HOME ?? (inheritedHub ? dirname6(resolve8(inheritedHub)) : join6(homedir3(), ".cache", "huggingface"));
|
|
2854
3678
|
const layout = resolveHuggingFaceCacheLayout(canonicalHome);
|
|
2855
3679
|
const result = {
|
|
2856
3680
|
...env,
|
|
@@ -2930,10 +3754,10 @@ function certifiedFileDigests(modelId) {
|
|
|
2930
3754
|
}
|
|
2931
3755
|
}
|
|
2932
3756
|
async function certifiedFileDigest(path, expected) {
|
|
2933
|
-
const metadata = await
|
|
2934
|
-
const digest =
|
|
3757
|
+
const metadata = await stat5(path);
|
|
3758
|
+
const digest = createHash5(expected.algorithm);
|
|
2935
3759
|
if (expected.algorithm === "sha1") digest.update(`blob ${metadata.size}\0`);
|
|
2936
|
-
for await (const chunk of
|
|
3760
|
+
for await (const chunk of createReadStream3(path)) digest.update(chunk);
|
|
2937
3761
|
return digest.digest("hex");
|
|
2938
3762
|
}
|
|
2939
3763
|
function isStrictPhysicalDescendant(parent, child) {
|
|
@@ -2943,8 +3767,8 @@ function isStrictPhysicalDescendant(parent, child) {
|
|
|
2943
3767
|
async function verifySnapshotCacheLocation(path, modelId, revision, modelCache) {
|
|
2944
3768
|
const repository = `models--${modelId.replaceAll("/", "--")}`;
|
|
2945
3769
|
const layout = resolveHuggingFaceCacheLayout(modelCache);
|
|
2946
|
-
const repositoryRoot =
|
|
2947
|
-
const expectedPath =
|
|
3770
|
+
const repositoryRoot = join7(layout.hubCache, repository);
|
|
3771
|
+
const expectedPath = join7(repositoryRoot, "snapshots", revision);
|
|
2948
3772
|
const [
|
|
2949
3773
|
physicalCacheRoot,
|
|
2950
3774
|
physicalHubRoot,
|
|
@@ -2956,7 +3780,7 @@ async function verifySnapshotCacheLocation(path, modelId, revision, modelCache)
|
|
|
2956
3780
|
realpath2(layout.hfHome).catch(() => null),
|
|
2957
3781
|
realpath2(layout.hubCache).catch(() => null),
|
|
2958
3782
|
realpath2(repositoryRoot).catch(() => null),
|
|
2959
|
-
realpath2(
|
|
3783
|
+
realpath2(join7(repositoryRoot, "blobs")).catch(() => null),
|
|
2960
3784
|
realpath2(path).catch(() => null),
|
|
2961
3785
|
realpath2(expectedPath).catch(() => null)
|
|
2962
3786
|
]);
|
|
@@ -2972,7 +3796,7 @@ async function verifySnapshotCacheLocation(path, modelId, revision, modelCache)
|
|
|
2972
3796
|
throw new Error(`Hugging Face repository escapes its configured hub cache: ${repositoryRoot}`);
|
|
2973
3797
|
}
|
|
2974
3798
|
if (!isStrictPhysicalDescendant(physicalRepositoryRoot, physicalBlobRoot)) {
|
|
2975
|
-
throw new Error(`Hugging Face blob store escapes its repository: ${
|
|
3799
|
+
throw new Error(`Hugging Face blob store escapes its repository: ${join7(repositoryRoot, "blobs")}`);
|
|
2976
3800
|
}
|
|
2977
3801
|
if (!isStrictPhysicalDescendant(physicalRepositoryRoot, physicalExpectedPath)) {
|
|
2978
3802
|
throw new Error(`Hugging Face snapshot escapes its configured repository: ${expectedPath}`);
|
|
@@ -3034,13 +3858,13 @@ async function verifyCertifiedLocalSnapshot(path, modelId, snapshotFiles, modelC
|
|
|
3034
3858
|
const expectedFiles = new Set(Object.keys(digests));
|
|
3035
3859
|
const unexpected = [...actualFiles].filter((name) => !expectedFiles.has(name)).sort();
|
|
3036
3860
|
if (unexpected.length > 0) {
|
|
3037
|
-
throw new Error(`Certified snapshot contains unexpected file: ${
|
|
3861
|
+
throw new Error(`Certified snapshot contains unexpected file: ${join7(path, unexpected[0])}`);
|
|
3038
3862
|
}
|
|
3039
3863
|
for (const [name, expected] of Object.entries(digests)) {
|
|
3040
3864
|
if (!actualFiles.has(name)) {
|
|
3041
|
-
throw new Error(`Certified snapshot file is missing: ${
|
|
3865
|
+
throw new Error(`Certified snapshot file is missing: ${join7(path, name)}`);
|
|
3042
3866
|
}
|
|
3043
|
-
const file =
|
|
3867
|
+
const file = join7(path, name);
|
|
3044
3868
|
const physicalFile = physicalFiles.get(file);
|
|
3045
3869
|
if (await certifiedFileDigest(physicalFile, expected) !== expected.digest) {
|
|
3046
3870
|
throw new Error(`Certified snapshot file checksum mismatch: ${file}`);
|
|
@@ -3083,9 +3907,9 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3083
3907
|
const files = [];
|
|
3084
3908
|
const visit = async (directory) => {
|
|
3085
3909
|
for (const entry of await readdir4(directory, { withFileTypes: true })) {
|
|
3086
|
-
const child =
|
|
3910
|
+
const child = join7(directory, entry.name);
|
|
3087
3911
|
if (entry.isSymbolicLink()) {
|
|
3088
|
-
const metadata2 = await
|
|
3912
|
+
const metadata2 = await stat5(child);
|
|
3089
3913
|
if (!metadata2.isFile()) {
|
|
3090
3914
|
throw new Error(`Local base model contains a non-file symbolic link: ${child}`);
|
|
3091
3915
|
}
|
|
@@ -3096,7 +3920,7 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3096
3920
|
await visit(child);
|
|
3097
3921
|
continue;
|
|
3098
3922
|
}
|
|
3099
|
-
const metadata = await
|
|
3923
|
+
const metadata = await stat5(child);
|
|
3100
3924
|
if (metadata.isFile()) files.push({ path: child, size: metadata.size });
|
|
3101
3925
|
}
|
|
3102
3926
|
};
|
|
@@ -3107,15 +3931,15 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3107
3931
|
throw new Error(`Local base-model directory contains no non-empty Transformers model weights: ${path}`);
|
|
3108
3932
|
}
|
|
3109
3933
|
const required = async (name) => {
|
|
3110
|
-
const metadata = await
|
|
3934
|
+
const metadata = await stat5(join7(path, name)).catch(() => null);
|
|
3111
3935
|
return Boolean(metadata?.isFile() && metadata.size > 0);
|
|
3112
3936
|
};
|
|
3113
3937
|
if (!await required("config.json")) throw new Error(`Local base-model directory is missing config.json: ${path}`);
|
|
3114
3938
|
try {
|
|
3115
|
-
const config = JSON.parse(await
|
|
3939
|
+
const config = JSON.parse(await readFile6(join7(path, "config.json"), "utf8"));
|
|
3116
3940
|
assertCertifiedBaseModelConfig(
|
|
3117
3941
|
config,
|
|
3118
|
-
`Local base-model config ${
|
|
3942
|
+
`Local base-model config ${join7(path, "config.json")}`,
|
|
3119
3943
|
expectedModelId
|
|
3120
3944
|
);
|
|
3121
3945
|
} catch (error) {
|
|
@@ -3144,13 +3968,17 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3144
3968
|
if (!await required("tokenizer_config.json") || !(await Promise.all(vocabNames.map(required))).some(Boolean)) {
|
|
3145
3969
|
throw new Error(`Local base-model directory is missing tokenizer metadata or vocabulary: ${path}`);
|
|
3146
3970
|
}
|
|
3971
|
+
const inventory = new Set(nonEmpty.map((file) => resolve9(file.path)));
|
|
3147
3972
|
for (const file of files.filter((candidate) => candidate.path.endsWith(".index.json"))) {
|
|
3148
|
-
const parsed = JSON.parse(await
|
|
3973
|
+
const parsed = JSON.parse(await readFile6(file.path, "utf8"));
|
|
3149
3974
|
if (!parsed.weight_map || typeof parsed.weight_map !== "object" || Array.isArray(parsed.weight_map)) {
|
|
3150
3975
|
throw new Error(`Local base-model weight index is invalid: ${file.path}`);
|
|
3151
3976
|
}
|
|
3152
3977
|
for (const shard of new Set(Object.values(parsed.weight_map))) {
|
|
3153
|
-
if (typeof shard !== "string" || !
|
|
3978
|
+
if (typeof shard !== "string" || !shard || /^(?:[A-Za-z]:|[/\\])/.test(shard) || shard.split(/[/\\]/).includes("..")) {
|
|
3979
|
+
throw new Error("Local base-model weight index has an invalid indexed weight shard path");
|
|
3980
|
+
}
|
|
3981
|
+
if (!inventory.has(resolve9(path, shard)) || !await required(shard)) {
|
|
3154
3982
|
throw new Error(`Local base-model directory is missing indexed weight shard: ${String(shard)}`);
|
|
3155
3983
|
}
|
|
3156
3984
|
}
|
|
@@ -3170,7 +3998,7 @@ async function prefetchBaseModel(args) {
|
|
|
3170
3998
|
args.localOnly ? "verify-base" : "prefetch",
|
|
3171
3999
|
`${safeName(args.request.spec_snapshot.base_model)}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`
|
|
3172
4000
|
);
|
|
3173
|
-
await
|
|
4001
|
+
await mkdir6(artifactDir, { recursive: true });
|
|
3174
4002
|
if (args.config.paths.baseModel) {
|
|
3175
4003
|
const localPath = resolve9(args.config.paths.baseModel);
|
|
3176
4004
|
const verified = await verifyLocalBaseModel(
|
|
@@ -3189,9 +4017,9 @@ async function prefetchBaseModel(args) {
|
|
|
3189
4017
|
reason: args.localOnly ? "Verified the configured local base-model artifact; no network access was used." : "paths.baseModel points at a verified local base-model artifact; no Hugging Face download is needed."
|
|
3190
4018
|
};
|
|
3191
4019
|
}
|
|
3192
|
-
const inputPath =
|
|
3193
|
-
const outputPath =
|
|
3194
|
-
const logPath =
|
|
4020
|
+
const inputPath = join7(artifactDir, "prefetch-input.json");
|
|
4021
|
+
const outputPath = join7(artifactDir, "prefetch-output.json");
|
|
4022
|
+
const logPath = join7(artifactDir, "prefetch.log");
|
|
3195
4023
|
await writeJson(inputPath, payload);
|
|
3196
4024
|
const entrypoint = buildBundledPythonCommand(
|
|
3197
4025
|
"prefetch.py",
|
|
@@ -3228,7 +4056,7 @@ async function prefetchBaseModel(args) {
|
|
|
3228
4056
|
if (exitCode !== 0) {
|
|
3229
4057
|
throw new Error(`Model prefetch exited with code ${exitCode}. See ${logPath}.`);
|
|
3230
4058
|
}
|
|
3231
|
-
const output = JSON.parse(await
|
|
4059
|
+
const output = JSON.parse(await readFile6(outputPath, "utf8"));
|
|
3232
4060
|
const snapshotRevision = baseModelRevisionSchema.safeParse(output.snapshot_revision);
|
|
3233
4061
|
if (!snapshotRevision.success) {
|
|
3234
4062
|
throw new Error(
|
|
@@ -3291,6 +4119,346 @@ async function prefetchBaseModel(args) {
|
|
|
3291
4119
|
};
|
|
3292
4120
|
}
|
|
3293
4121
|
|
|
4122
|
+
// src/local-runtime/dataset-prefetch.ts
|
|
4123
|
+
import { createHash as createHash6 } from "crypto";
|
|
4124
|
+
import { createReadStream as createReadStream4 } from "fs";
|
|
4125
|
+
import { mkdir as mkdir7, readFile as readFile7, realpath as realpath3, stat as stat6 } from "fs/promises";
|
|
4126
|
+
import { createInterface } from "readline";
|
|
4127
|
+
import { basename, isAbsolute as isAbsolute5, join as join8, relative as relative4, resolve as resolve10, sep as sep5 } from "path";
|
|
4128
|
+
var SPLITS = ["training", "validation", "test"];
|
|
4129
|
+
var DatasetNotDownloadedError = class extends Error {
|
|
4130
|
+
constructor(message) {
|
|
4131
|
+
super(message);
|
|
4132
|
+
this.name = "DatasetNotDownloadedError";
|
|
4133
|
+
}
|
|
4134
|
+
};
|
|
4135
|
+
function huggingFaceDataset(request) {
|
|
4136
|
+
const dataset = request.dataset_prebuilt;
|
|
4137
|
+
return dataset?.huggingface ? dataset : void 0;
|
|
4138
|
+
}
|
|
4139
|
+
function datasetFiles(dataset) {
|
|
4140
|
+
const files = {};
|
|
4141
|
+
for (const split of SPLITS) if (dataset[split]) files[split] = dataset[split];
|
|
4142
|
+
return files;
|
|
4143
|
+
}
|
|
4144
|
+
function huggingFaceHubCache(modelCache) {
|
|
4145
|
+
return withHuggingFaceCacheEnvironment(process.env, modelCache).HF_HUB_CACHE;
|
|
4146
|
+
}
|
|
4147
|
+
function repositoryDirectory(hubCache, repo) {
|
|
4148
|
+
return join8(hubCache, `datasets--${repo.replaceAll("/", "--")}`);
|
|
4149
|
+
}
|
|
4150
|
+
function isStrictDescendant(parent, child) {
|
|
4151
|
+
const path = relative4(parent, child);
|
|
4152
|
+
return path !== "" && path !== ".." && !path.startsWith(`..${sep5}`) && !isAbsolute5(path);
|
|
4153
|
+
}
|
|
4154
|
+
async function digestFile(path, size) {
|
|
4155
|
+
const sha256 = createHash6("sha256");
|
|
4156
|
+
const gitSha1 = createHash6("sha1").update(`blob ${size}\0`);
|
|
4157
|
+
for await (const chunk of createReadStream4(path)) {
|
|
4158
|
+
sha256.update(chunk);
|
|
4159
|
+
gitSha1.update(chunk);
|
|
4160
|
+
}
|
|
4161
|
+
return { sha256: sha256.digest("hex"), gitSha1: gitSha1.digest("hex") };
|
|
4162
|
+
}
|
|
4163
|
+
function describe(dataset) {
|
|
4164
|
+
return `${dataset.repo}@${dataset.revision ?? "main"}`;
|
|
4165
|
+
}
|
|
4166
|
+
async function resolveCachedHuggingFaceDataset(args) {
|
|
4167
|
+
const source = args.dataset.huggingface;
|
|
4168
|
+
const hubCache = huggingFaceHubCache(args.modelCache);
|
|
4169
|
+
const repository = repositoryDirectory(hubCache, source.repo);
|
|
4170
|
+
const prefetchHint = "Run `tt datasets prefetch` with this spec (for example `tt datasets prefetch tunedtensor.json`) to download it.";
|
|
4171
|
+
let revision = source.revision;
|
|
4172
|
+
if (!revision) {
|
|
4173
|
+
const ref = await readFile7(join8(repository, "refs", "main"), "utf8").catch(() => null);
|
|
4174
|
+
const parsed = baseModelRevisionSchema.safeParse(ref?.trim());
|
|
4175
|
+
if (!parsed.success) {
|
|
4176
|
+
throw new DatasetNotDownloadedError(
|
|
4177
|
+
`Hugging Face dataset ${describe(source)} is not in the local cache (${hubCache}). ${prefetchHint}`
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
revision = parsed.data;
|
|
4181
|
+
}
|
|
4182
|
+
const snapshot = join8(repository, "snapshots", revision);
|
|
4183
|
+
const physicalRepository = await realpath3(repository).catch(() => null);
|
|
4184
|
+
const blobs = physicalRepository ? join8(physicalRepository, "blobs") : null;
|
|
4185
|
+
const files = {};
|
|
4186
|
+
const localPaths = {};
|
|
4187
|
+
for (const [split, path] of Object.entries(datasetFiles(args.dataset))) {
|
|
4188
|
+
const local = join8(snapshot, ...path.split(/[/\\]/));
|
|
4189
|
+
const metadata = await stat6(local).catch(() => null);
|
|
4190
|
+
if (!metadata || !physicalRepository || !blobs) {
|
|
4191
|
+
throw new DatasetNotDownloadedError(
|
|
4192
|
+
`Hugging Face dataset file ${path} (${split}) from ${source.repo}@${revision} is not in the local cache (${hubCache}). ${prefetchHint}`
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
const target = await realpath3(local);
|
|
4196
|
+
if (!metadata.isFile() || !isStrictDescendant(physicalRepository, target)) {
|
|
4197
|
+
throw new Error(`Cached dataset file ${local} must resolve inside its Hugging Face cache repository ${physicalRepository}.`);
|
|
4198
|
+
}
|
|
4199
|
+
if (metadata.size === 0) throw new Error(`Cached dataset file ${path} (${split}) is empty: ${local}`);
|
|
4200
|
+
const digests = await digestFile(target, metadata.size);
|
|
4201
|
+
const blobName = isStrictDescendant(blobs, target) ? basename(target).toLowerCase() : "";
|
|
4202
|
+
const expected = /^[0-9a-f]{64}$/.test(blobName) ? digests.sha256 : /^[0-9a-f]{40}$/.test(blobName) ? digests.gitSha1 : null;
|
|
4203
|
+
if (expected !== null && expected !== blobName) {
|
|
4204
|
+
throw new Error(
|
|
4205
|
+
`Cached dataset file ${path} (${split}) failed its checksum (${local}). Delete ${target} and run \`tt datasets prefetch\` again.`
|
|
4206
|
+
);
|
|
4207
|
+
}
|
|
4208
|
+
files[split] = { path, size_bytes: metadata.size, sha256: digests.sha256 };
|
|
4209
|
+
localPaths[split] = local;
|
|
4210
|
+
}
|
|
4211
|
+
return {
|
|
4212
|
+
source: datasetSourceSchema.parse({
|
|
4213
|
+
provider: "huggingface",
|
|
4214
|
+
repo: source.repo,
|
|
4215
|
+
requested_revision: source.revision ?? null,
|
|
4216
|
+
revision,
|
|
4217
|
+
pinned: Boolean(source.revision),
|
|
4218
|
+
columns: source.columns ?? null,
|
|
4219
|
+
snapshot_path: snapshot,
|
|
4220
|
+
files
|
|
4221
|
+
}),
|
|
4222
|
+
localPaths
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
function mappedField(row, name) {
|
|
4226
|
+
const value = row[name];
|
|
4227
|
+
if (typeof value === "string") return value.trim() ? value : null;
|
|
4228
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4229
|
+
return null;
|
|
4230
|
+
}
|
|
4231
|
+
async function readMappedRecords(path, split, columns) {
|
|
4232
|
+
const examples = [];
|
|
4233
|
+
let records = 0;
|
|
4234
|
+
let skipped = 0;
|
|
4235
|
+
const availableFields = /* @__PURE__ */ new Set();
|
|
4236
|
+
const lines = createInterface({ input: createReadStream4(path, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
4237
|
+
let index = -1;
|
|
4238
|
+
for await (const line of lines) {
|
|
4239
|
+
index += 1;
|
|
4240
|
+
if (!line.trim()) continue;
|
|
4241
|
+
records += 1;
|
|
4242
|
+
let value;
|
|
4243
|
+
try {
|
|
4244
|
+
value = JSON.parse(line);
|
|
4245
|
+
} catch (error) {
|
|
4246
|
+
throw new Error(`${split} line ${index + 1}: malformed JSON in ${path}`, { cause: error });
|
|
4247
|
+
}
|
|
4248
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4249
|
+
throw new Error(`${split} line ${index + 1}: expected a JSON object in ${path}`);
|
|
4250
|
+
}
|
|
4251
|
+
const row = value;
|
|
4252
|
+
for (const name of Object.keys(row)) availableFields.add(name);
|
|
4253
|
+
const input = mappedField(row, columns.input);
|
|
4254
|
+
const output = mappedField(row, columns.output);
|
|
4255
|
+
if (input === null || output === null) {
|
|
4256
|
+
skipped += 1;
|
|
4257
|
+
continue;
|
|
4258
|
+
}
|
|
4259
|
+
examples.push({ input, output });
|
|
4260
|
+
}
|
|
4261
|
+
if (records > 0) {
|
|
4262
|
+
for (const name of [columns.input, columns.output]) {
|
|
4263
|
+
if (!availableFields.has(name)) {
|
|
4264
|
+
throw new Error(
|
|
4265
|
+
`${split} records have no "${name}" field (dataset_prebuilt.huggingface.columns). Available fields: ${[...availableFields].join(", ")}`
|
|
4266
|
+
);
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
return { examples, records, skipped };
|
|
4271
|
+
}
|
|
4272
|
+
async function convertMappedDataset(args) {
|
|
4273
|
+
const system = buildSystemMessage(args.spec);
|
|
4274
|
+
const splits = Object.keys(args.resolved.localPaths);
|
|
4275
|
+
const key = createHash6("sha256").update(JSON.stringify({
|
|
4276
|
+
version: 1,
|
|
4277
|
+
sources: splits.map((split) => [split, args.resolved.source.files[split]?.sha256]),
|
|
4278
|
+
columns: args.columns,
|
|
4279
|
+
system
|
|
4280
|
+
})).digest("hex").slice(0, 16);
|
|
4281
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
4282
|
+
for (const split of splits) {
|
|
4283
|
+
loaded.set(split, await readMappedRecords(args.resolved.localPaths[split], split, args.columns));
|
|
4284
|
+
}
|
|
4285
|
+
const evaluationInputs = /* @__PURE__ */ new Set();
|
|
4286
|
+
const kept = /* @__PURE__ */ new Map();
|
|
4287
|
+
const conversion = {};
|
|
4288
|
+
for (const split of splits.filter((name) => name !== "training")) {
|
|
4289
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4290
|
+
const data = loaded.get(split);
|
|
4291
|
+
const examples = data.examples.filter((example) => {
|
|
4292
|
+
const identity = datasetInputIdentity(example.input);
|
|
4293
|
+
if (seen.has(identity)) return false;
|
|
4294
|
+
seen.add(identity);
|
|
4295
|
+
evaluationInputs.add(identity);
|
|
4296
|
+
return true;
|
|
4297
|
+
});
|
|
4298
|
+
kept.set(split, examples);
|
|
4299
|
+
conversion[split] = {
|
|
4300
|
+
records: data.records,
|
|
4301
|
+
kept: examples.length,
|
|
4302
|
+
skipped_missing_fields: data.skipped,
|
|
4303
|
+
dropped_duplicate_inputs: data.examples.length - examples.length,
|
|
4304
|
+
dropped_eval_overlap: 0
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
const training = loaded.get("training");
|
|
4308
|
+
const trainingExamples = training.examples.filter((example) => !evaluationInputs.has(datasetInputIdentity(example.input)));
|
|
4309
|
+
kept.set("training", trainingExamples);
|
|
4310
|
+
conversion.training = {
|
|
4311
|
+
records: training.records,
|
|
4312
|
+
kept: trainingExamples.length,
|
|
4313
|
+
skipped_missing_fields: training.skipped,
|
|
4314
|
+
dropped_duplicate_inputs: 0,
|
|
4315
|
+
dropped_eval_overlap: training.examples.length - trainingExamples.length
|
|
4316
|
+
};
|
|
4317
|
+
await mkdir7(args.outputRoot, { recursive: true });
|
|
4318
|
+
const localPaths = {};
|
|
4319
|
+
for (const split of splits) {
|
|
4320
|
+
const examples = kept.get(split);
|
|
4321
|
+
if (examples.length === 0) {
|
|
4322
|
+
throw new Error(`No usable ${split} records remain after applying dataset_prebuilt.huggingface.columns.`);
|
|
4323
|
+
}
|
|
4324
|
+
const output = join8(args.outputRoot, `${split}-${key}.jsonl`);
|
|
4325
|
+
const content = `${examples.map((example) => JSON.stringify(exampleToChatRow(args.spec, example))).join("\n")}
|
|
4326
|
+
`;
|
|
4327
|
+
if (await readFile7(output, "utf8").catch(() => null) !== content) {
|
|
4328
|
+
await writeFileAtomic(output, content);
|
|
4329
|
+
}
|
|
4330
|
+
localPaths[split] = output;
|
|
4331
|
+
}
|
|
4332
|
+
return { localPaths, conversion };
|
|
4333
|
+
}
|
|
4334
|
+
async function resolveRequestDataset(request, config) {
|
|
4335
|
+
const dataset = huggingFaceDataset(request);
|
|
4336
|
+
if (!dataset) return request;
|
|
4337
|
+
const resolved = await resolveCachedHuggingFaceDataset({ dataset, modelCache: config.paths.modelCache });
|
|
4338
|
+
let localPaths = resolved.localPaths;
|
|
4339
|
+
let source = resolved.source;
|
|
4340
|
+
const columns = dataset.huggingface.columns;
|
|
4341
|
+
if (columns) {
|
|
4342
|
+
const converted = await convertMappedDataset({
|
|
4343
|
+
resolved,
|
|
4344
|
+
columns,
|
|
4345
|
+
spec: request.spec_snapshot,
|
|
4346
|
+
outputRoot: resolve10(config.artifactRoot, "datasets", "huggingface", safeName(dataset.huggingface.repo), resolved.source.revision)
|
|
4347
|
+
});
|
|
4348
|
+
localPaths = converted.localPaths;
|
|
4349
|
+
source = datasetSourceSchema.parse({ ...source, conversion: converted.conversion });
|
|
4350
|
+
}
|
|
4351
|
+
return fineTuneRunRequestSchema.parse({
|
|
4352
|
+
...request,
|
|
4353
|
+
dataset_prebuilt: {
|
|
4354
|
+
...localPaths,
|
|
4355
|
+
format: dataset.format
|
|
4356
|
+
},
|
|
4357
|
+
dataset_source: source
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
async function prefetchHuggingFaceDataset(args) {
|
|
4361
|
+
const dataset = huggingFaceDataset(args.request);
|
|
4362
|
+
if (!dataset) {
|
|
4363
|
+
throw new Error(
|
|
4364
|
+
"This spec has no dataset_prebuilt.huggingface source. Local dataset files need no download; set dataset_prebuilt.huggingface.repo to train on a Hugging Face dataset."
|
|
4365
|
+
);
|
|
4366
|
+
}
|
|
4367
|
+
const modelCache = args.config.paths.modelCache ? resolve10(args.config.paths.modelCache) : void 0;
|
|
4368
|
+
const hubCache = huggingFaceHubCache(modelCache);
|
|
4369
|
+
if (args.localOnly) {
|
|
4370
|
+
const resolved2 = await resolveCachedHuggingFaceDataset({ dataset, modelCache });
|
|
4371
|
+
return {
|
|
4372
|
+
ok: true,
|
|
4373
|
+
status: "verified",
|
|
4374
|
+
local_files_only: true,
|
|
4375
|
+
hub_cache: hubCache,
|
|
4376
|
+
size_bytes: Object.values(resolved2.source.files).reduce((total, file) => total + (file?.size_bytes ?? 0), 0),
|
|
4377
|
+
...resolved2.source
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
const artifactDir = resolve10(
|
|
4381
|
+
args.config.artifactRoot,
|
|
4382
|
+
"dataset-prefetch",
|
|
4383
|
+
`${safeName(dataset.huggingface.repo)}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`
|
|
4384
|
+
);
|
|
4385
|
+
await mkdir7(artifactDir, { recursive: true });
|
|
4386
|
+
const inputPath = join8(artifactDir, "prefetch-input.json");
|
|
4387
|
+
const outputPath = join8(artifactDir, "prefetch-output.json");
|
|
4388
|
+
const logPath = join8(artifactDir, "prefetch.log");
|
|
4389
|
+
await writeJson(inputPath, {
|
|
4390
|
+
repo: dataset.huggingface.repo,
|
|
4391
|
+
...dataset.huggingface.revision ? { revision: dataset.huggingface.revision } : {},
|
|
4392
|
+
files: datasetFiles(dataset),
|
|
4393
|
+
...modelCache ? { model_cache: modelCache } : {}
|
|
4394
|
+
});
|
|
4395
|
+
const entrypoint = buildBundledPythonCommand("prefetch_dataset.py", ["--input", inputPath, "--output", outputPath]);
|
|
4396
|
+
await args.reporter?.onEvent?.({
|
|
4397
|
+
stage: "dataset_prefetch",
|
|
4398
|
+
status: "running",
|
|
4399
|
+
message: "Downloading Hugging Face dataset files.",
|
|
4400
|
+
details: { repo: dataset.huggingface.repo, revision: dataset.huggingface.revision ?? "main", hub_cache: hubCache, log_path: logPath }
|
|
4401
|
+
});
|
|
4402
|
+
const { exitCode, stderr } = await runLoggedProcess({
|
|
4403
|
+
command: entrypoint.command,
|
|
4404
|
+
commandArgs: entrypoint.commandArgs,
|
|
4405
|
+
env: withBundledPythonEnvironment(withHuggingFaceCacheEnvironment(minimalMachineLearningEnvironment(process.env), modelCache)),
|
|
4406
|
+
logPath,
|
|
4407
|
+
reporter: args.reporter ? { ...args.reporter, verbose: true } : void 0,
|
|
4408
|
+
stage: "dataset_prefetch"
|
|
4409
|
+
});
|
|
4410
|
+
if (exitCode !== 0) {
|
|
4411
|
+
const reason = stderr.trim().split("\n").filter(Boolean).at(-1);
|
|
4412
|
+
throw new Error(
|
|
4413
|
+
`Dataset prefetch exited with code ${exitCode}${reason ? `: ${reason}` : ""}. See ${logPath}. Rerunning resumes from the files already downloaded.`
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
const output = JSON.parse(await readFile7(outputPath, "utf8"));
|
|
4417
|
+
const revision = baseModelRevisionSchema.parse(output.snapshot_revision);
|
|
4418
|
+
const resolved = await resolveCachedHuggingFaceDataset({
|
|
4419
|
+
dataset: { ...dataset, huggingface: { ...dataset.huggingface, revision } },
|
|
4420
|
+
modelCache
|
|
4421
|
+
});
|
|
4422
|
+
for (const [split, file] of Object.entries(resolved.source.files)) {
|
|
4423
|
+
if (output.files?.[split]?.sha256 !== file?.sha256) {
|
|
4424
|
+
throw new Error(`Dataset prefetch reported a different digest for ${split} than the cached file.`);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
const source = datasetSourceSchema.parse({
|
|
4428
|
+
...resolved.source,
|
|
4429
|
+
requested_revision: dataset.huggingface.revision ?? null,
|
|
4430
|
+
pinned: Boolean(dataset.huggingface.revision)
|
|
4431
|
+
});
|
|
4432
|
+
const sizeBytes = Object.values(source.files).reduce((total, file) => total + (file?.size_bytes ?? 0), 0);
|
|
4433
|
+
await args.reporter?.onEvent?.({
|
|
4434
|
+
stage: "dataset_prefetch",
|
|
4435
|
+
status: "completed",
|
|
4436
|
+
message: "Dataset files are available in the local Hugging Face cache.",
|
|
4437
|
+
details: { repo: source.repo, revision: source.revision, pinned: source.pinned, size_bytes: sizeBytes }
|
|
4438
|
+
});
|
|
4439
|
+
return {
|
|
4440
|
+
ok: true,
|
|
4441
|
+
status: "completed",
|
|
4442
|
+
local_files_only: false,
|
|
4443
|
+
hub_cache: hubCache,
|
|
4444
|
+
size_bytes: sizeBytes,
|
|
4445
|
+
...source,
|
|
4446
|
+
artifact_dir: artifactDir,
|
|
4447
|
+
log_uri: fileUri(logPath),
|
|
4448
|
+
command: entrypoint.displayCommand
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
async function ensureDatasetCached(args) {
|
|
4452
|
+
if (!huggingFaceDataset(args.request)) return { request: args.request, prefetch: null };
|
|
4453
|
+
try {
|
|
4454
|
+
return { request: await resolveRequestDataset(args.request, args.config), prefetch: null };
|
|
4455
|
+
} catch (error) {
|
|
4456
|
+
if (!(error instanceof DatasetNotDownloadedError) || args.config.dryRun) throw error;
|
|
4457
|
+
}
|
|
4458
|
+
const prefetch = await prefetchHuggingFaceDataset(args);
|
|
4459
|
+
return { request: await resolveRequestDataset(args.request, args.config), prefetch };
|
|
4460
|
+
}
|
|
4461
|
+
|
|
3294
4462
|
// src/local-runtime/general-regression.ts
|
|
3295
4463
|
function evaluateGeneralRegressionGate(comparison, policy) {
|
|
3296
4464
|
const failures = [];
|
|
@@ -3311,18 +4479,18 @@ function evaluateGeneralRegressionGate(comparison, policy) {
|
|
|
3311
4479
|
}
|
|
3312
4480
|
|
|
3313
4481
|
// src/local-runtime/orchestrator.ts
|
|
3314
|
-
import { lstat as lstat5, mkdir as
|
|
3315
|
-
import { createReadStream as
|
|
3316
|
-
import { createHash as
|
|
4482
|
+
import { lstat as lstat5, mkdir as mkdir11, open as open4, readFile as readFile10, readdir as readdir5, rename as rename4, rm as rm5, stat as stat9 } from "fs/promises";
|
|
4483
|
+
import { createReadStream as createReadStream5 } from "fs";
|
|
4484
|
+
import { createHash as createHash9, randomUUID as randomUUID6 } from "crypto";
|
|
3317
4485
|
import { performance } from "perf_hooks";
|
|
3318
4486
|
import { homedir as homedir4 } from "os";
|
|
3319
|
-
import { basename as
|
|
4487
|
+
import { basename as basename3, dirname as dirname9, join as join12, relative as relative5, resolve as resolve14 } from "path";
|
|
3320
4488
|
|
|
3321
4489
|
// src/local-runtime/gpu-executor.ts
|
|
3322
|
-
import { createHash as
|
|
3323
|
-
import { mkdtemp, mkdir as
|
|
4490
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
4491
|
+
import { mkdtemp, mkdir as mkdir8, realpath as realpath4, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
|
|
3324
4492
|
import { tmpdir } from "os";
|
|
3325
|
-
import { basename, dirname as dirname7, join as
|
|
4493
|
+
import { basename as basename2, dirname as dirname7, join as join9, resolve as resolve11 } from "path";
|
|
3326
4494
|
function shellQuote(value) {
|
|
3327
4495
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
3328
4496
|
}
|
|
@@ -3387,23 +4555,23 @@ async function checkAwsGpu(gpu) {
|
|
|
3387
4555
|
if (result.exitCode !== 0) throw new Error(`AWS GPU preflight failed: ${result.stderr}`);
|
|
3388
4556
|
}
|
|
3389
4557
|
function gpuBaseModelPath(config, model, revision) {
|
|
3390
|
-
if (config.paths.baseModel) return
|
|
4558
|
+
if (config.paths.baseModel) return resolve11(config.paths.baseModel);
|
|
3391
4559
|
if (!revision || !/^[a-f0-9]{40}$/i.test(revision)) {
|
|
3392
4560
|
throw new Error("AWS GPU execution needs an immutable base-model revision. Run tt models prefetch first or configure paths.baseModel.");
|
|
3393
4561
|
}
|
|
3394
4562
|
const env = withHuggingFaceCacheEnvironment(process.env, config.paths.modelCache);
|
|
3395
|
-
return
|
|
4563
|
+
return join9(env.HF_HUB_CACHE, `models--${model.replaceAll("/", "--")}`, "snapshots", revision);
|
|
3396
4564
|
}
|
|
3397
4565
|
async function physicalTransferPath(path) {
|
|
3398
4566
|
try {
|
|
3399
|
-
return await
|
|
4567
|
+
return await realpath4(path);
|
|
3400
4568
|
} catch (error) {
|
|
3401
4569
|
if (error.code !== "ENOENT" || dirname7(path) === path) throw error;
|
|
3402
|
-
return
|
|
4570
|
+
return join9(await physicalTransferPath(dirname7(path)), basename2(path));
|
|
3403
4571
|
}
|
|
3404
4572
|
}
|
|
3405
4573
|
async function assertSeparateTransferPaths(files) {
|
|
3406
|
-
const paths = await Promise.all(files.map((file) => physicalTransferPath(
|
|
4574
|
+
const paths = await Promise.all(files.map((file) => physicalTransferPath(resolve11(file.path))));
|
|
3407
4575
|
for (let i = 0; i < files.length; i += 1) {
|
|
3408
4576
|
for (let j = 0; j < i; j += 1) {
|
|
3409
4577
|
if (files[i].direction === "input" && files[j].direction === "input") continue;
|
|
@@ -3420,13 +4588,13 @@ async function runGpuProcess(args) {
|
|
|
3420
4588
|
await assertSeparateTransferPaths(args.files);
|
|
3421
4589
|
const host = await resolveGpuHost(args.gpu);
|
|
3422
4590
|
const options = sshOptions(args.gpu);
|
|
3423
|
-
const identity = args.logPath ?
|
|
4591
|
+
const identity = args.logPath ? createHash7("sha256").update(resolve11(args.logPath)).digest("hex").slice(0, 32) : randomUUID5();
|
|
3424
4592
|
const root = `/tmp/tt-gpu-${identity}`;
|
|
3425
|
-
const scratch = await mkdtemp(
|
|
3426
|
-
const runtime =
|
|
3427
|
-
const mappings = args.files.map((file, index) => ({ ...file, path:
|
|
4593
|
+
const scratch = await mkdtemp(join9(tmpdir(), "tt-gpu-"));
|
|
4594
|
+
const runtime = join9(localRuntimePackageRoot(import.meta.url), "training", args.runtime);
|
|
4595
|
+
const mappings = args.files.map((file, index) => ({ ...file, path: resolve11(file.path), remote: `${root}/files/${index}/${basename2(file.path)}` }));
|
|
3428
4596
|
const mapPath = (path) => {
|
|
3429
|
-
const absolute =
|
|
4597
|
+
const absolute = resolve11(path);
|
|
3430
4598
|
const match = [...mappings].sort((a, b) => b.path.length - a.path.length).find((file) => absolute === file.path || file.directory && absolute.startsWith(`${file.path}/`));
|
|
3431
4599
|
if (match) return match.remote + absolute.slice(match.path.length);
|
|
3432
4600
|
if (absolute === runtime || absolute.startsWith(`${runtime}/`)) return `${root}/runtime${absolute.slice(runtime.length)}`;
|
|
@@ -3464,7 +4632,7 @@ async function runGpuProcess(args) {
|
|
|
3464
4632
|
};
|
|
3465
4633
|
const grace = Math.ceil((args.shutdownGraceMs ?? 5e3) / 1e3);
|
|
3466
4634
|
const stop = `kill -TERM -- -"$pid" 2>/dev/null || true; remaining=${grace}; while kill -0 -- -"$pid" 2>/dev/null && [ "$remaining" -gt 0 ]; do sleep 1; remaining=$((remaining - 1)); done; kill -KILL -- -"$pid" 2>/dev/null || true`;
|
|
3467
|
-
const owner =
|
|
4635
|
+
const owner = randomUUID5();
|
|
3468
4636
|
let reservationAttempted = false;
|
|
3469
4637
|
let started = false;
|
|
3470
4638
|
let completed = false;
|
|
@@ -3475,12 +4643,12 @@ async function runGpuProcess(args) {
|
|
|
3475
4643
|
throw new Error(`Cannot reserve GPU staging directory ${host}:${root}. Another process or unrecovered artifacts may be present. ${error instanceof Error ? error.message : error}`);
|
|
3476
4644
|
});
|
|
3477
4645
|
for (const name of ["pyproject.toml", "uv.lock", "src"]) {
|
|
3478
|
-
await sync(
|
|
4646
|
+
await sync(join9(runtime, name), `${root}/runtime/`, true);
|
|
3479
4647
|
}
|
|
3480
4648
|
for (const file of mappings) {
|
|
3481
4649
|
await ssh(`mkdir -p ${shellQuote(file.directory ? file.remote : dirname7(file.remote))}`);
|
|
3482
4650
|
if (file.direction !== "output") {
|
|
3483
|
-
const exists3 = await
|
|
4651
|
+
const exists3 = await stat7(file.path).catch((error) => {
|
|
3484
4652
|
if (error.code === "ENOENT" && file.direction === "both") return void 0;
|
|
3485
4653
|
throw error;
|
|
3486
4654
|
});
|
|
@@ -3490,8 +4658,8 @@ async function runGpuProcess(args) {
|
|
|
3490
4658
|
if (args.document) {
|
|
3491
4659
|
const value = { ...args.document.value };
|
|
3492
4660
|
for (const key of args.document.pathKeys) if (typeof value[key] === "string") value[key] = mapPath(value[key]);
|
|
3493
|
-
const path =
|
|
3494
|
-
await
|
|
4661
|
+
const path = join9(scratch, "document.json");
|
|
4662
|
+
await writeFile5(path, JSON.stringify(value), { mode: 384 });
|
|
3495
4663
|
await sync(path, mapPath(args.document.path), true);
|
|
3496
4664
|
}
|
|
3497
4665
|
const env = {
|
|
@@ -3523,7 +4691,7 @@ async function runGpuProcess(args) {
|
|
|
3523
4691
|
if (started) {
|
|
3524
4692
|
await ssh(`bash -c ${shellQuote(`if test -f ${root}/pid; then pid=$(cat ${root}/pid); ${stop}; fi`)}`, false);
|
|
3525
4693
|
for (const file of mappings.filter((file2) => file2.direction !== "input")) {
|
|
3526
|
-
await
|
|
4694
|
+
await mkdir8(file.directory ? file.path : dirname7(file.path), { recursive: true });
|
|
3527
4695
|
if (!completed && !file.directory) {
|
|
3528
4696
|
const exists3 = await runLoggedProcess({ command: "ssh", commandArgs: [...options, host, `test -e ${shellQuote(file.remote)}`], stage: args.stage, timeoutMs: 3e4 });
|
|
3529
4697
|
if (exists3.exitCode === 1) continue;
|
|
@@ -3536,15 +4704,15 @@ async function runGpuProcess(args) {
|
|
|
3536
4704
|
} catch (error) {
|
|
3537
4705
|
throw new Error(`GPU cleanup or artifact retrieval failed. Remote files remain at ${host}:${root}. ${error instanceof Error ? error.message : error}`);
|
|
3538
4706
|
} finally {
|
|
3539
|
-
await
|
|
4707
|
+
await rm3(scratch, { recursive: true, force: true });
|
|
3540
4708
|
}
|
|
3541
4709
|
}
|
|
3542
4710
|
}
|
|
3543
4711
|
|
|
3544
4712
|
// src/local-runtime/evaluation.ts
|
|
3545
|
-
import { createHash as
|
|
3546
|
-
import { mkdir as
|
|
3547
|
-
import { dirname as dirname8, join as
|
|
4713
|
+
import { createHash as createHash8 } from "crypto";
|
|
4714
|
+
import { mkdir as mkdir9, readFile as readFile8, rm as rm4, stat as stat8, writeFile as writeFile6 } from "fs/promises";
|
|
4715
|
+
import { dirname as dirname8, join as join10, resolve as resolve12 } from "path";
|
|
3548
4716
|
function normalize(value) {
|
|
3549
4717
|
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
3550
4718
|
}
|
|
@@ -3638,11 +4806,11 @@ function extractJsonObject(value) {
|
|
|
3638
4806
|
}
|
|
3639
4807
|
return null;
|
|
3640
4808
|
}
|
|
3641
|
-
function
|
|
3642
|
-
if (Array.isArray(value)) return value.map(
|
|
4809
|
+
function canonicalJson4(value) {
|
|
4810
|
+
if (Array.isArray(value)) return value.map(canonicalJson4);
|
|
3643
4811
|
if (value && typeof value === "object") {
|
|
3644
4812
|
return Object.fromEntries(
|
|
3645
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key,
|
|
4813
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson4(item)])
|
|
3646
4814
|
);
|
|
3647
4815
|
}
|
|
3648
4816
|
return value;
|
|
@@ -3668,7 +4836,7 @@ function scoreJsonFields(expected, actual, configuredFields) {
|
|
|
3668
4836
|
const schemaMatch = Boolean(actualJson) && expectedKeys.length === actualKeys.length && expectedKeys.every((key, index) => key === actualKeys[index]);
|
|
3669
4837
|
const fieldResults = {};
|
|
3670
4838
|
for (const field of fields) {
|
|
3671
|
-
fieldResults[field] = Object.prototype.hasOwnProperty.call(expectedJson, field) && Boolean(actualJson) && JSON.stringify(
|
|
4839
|
+
fieldResults[field] = Object.prototype.hasOwnProperty.call(expectedJson, field) && Boolean(actualJson) && JSON.stringify(canonicalJson4(expectedJson[field])) === JSON.stringify(canonicalJson4(actualJson?.[field]));
|
|
3672
4840
|
}
|
|
3673
4841
|
const correct = Object.values(fieldResults).filter(Boolean).length;
|
|
3674
4842
|
const score = fields.length > 0 ? correct / fields.length : 0;
|
|
@@ -3764,8 +4932,8 @@ async function runTransformersInference(args) {
|
|
|
3764
4932
|
const inputPath = `${args.outputPath}.inference-input.json`;
|
|
3765
4933
|
const outputPath = `${args.outputPath}.inference-output.json`;
|
|
3766
4934
|
const logPath = `${args.outputPath}.inference.log`;
|
|
3767
|
-
await
|
|
3768
|
-
await
|
|
4935
|
+
await mkdir9(dirname8(inputPath), { recursive: true });
|
|
4936
|
+
await writeFile6(inputPath, `${JSON.stringify({
|
|
3769
4937
|
protocol_version: INFERENCE_PROTOCOL_VERSION,
|
|
3770
4938
|
kind: args.kind,
|
|
3771
4939
|
model_id: args.modelId,
|
|
@@ -3779,7 +4947,7 @@ async function runTransformersInference(args) {
|
|
|
3779
4947
|
id: String(index),
|
|
3780
4948
|
input: example.input
|
|
3781
4949
|
})),
|
|
3782
|
-
model_cache: args.config.paths.modelCache ?
|
|
4950
|
+
model_cache: args.config.paths.modelCache ? resolve12(args.config.paths.modelCache) : void 0,
|
|
3783
4951
|
trust_remote_code: false,
|
|
3784
4952
|
device: args.config.evaluation.inference.device,
|
|
3785
4953
|
generation: {
|
|
@@ -3789,7 +4957,7 @@ async function runTransformersInference(args) {
|
|
|
3789
4957
|
}
|
|
3790
4958
|
}, null, 2)}
|
|
3791
4959
|
`, "utf8");
|
|
3792
|
-
await
|
|
4960
|
+
await rm4(outputPath, { force: true });
|
|
3793
4961
|
const entrypoint = buildBundledPythonCommand(
|
|
3794
4962
|
"evaluate.py",
|
|
3795
4963
|
["--input", inputPath, "--output", outputPath]
|
|
@@ -3807,7 +4975,7 @@ async function runTransformersInference(args) {
|
|
|
3807
4975
|
});
|
|
3808
4976
|
const gpu = args.config.evaluation.inference.device === "cuda" ? args.config.gpu : void 0;
|
|
3809
4977
|
const execute = gpu ? async (processArgs) => {
|
|
3810
|
-
const document = JSON.parse(await
|
|
4978
|
+
const document = JSON.parse(await readFile8(inputPath, "utf8"));
|
|
3811
4979
|
document.model_source = gpuBaseModelPath(args.config, args.baseModelId, args.baseModelRevision);
|
|
3812
4980
|
delete document.model_cache;
|
|
3813
4981
|
const files = [
|
|
@@ -3817,7 +4985,7 @@ async function runTransformersInference(args) {
|
|
|
3817
4985
|
...document.adapter_path ? [{
|
|
3818
4986
|
path: document.adapter_path,
|
|
3819
4987
|
direction: "input",
|
|
3820
|
-
directory: (await
|
|
4988
|
+
directory: (await stat8(document.adapter_path)).isDirectory()
|
|
3821
4989
|
}] : []
|
|
3822
4990
|
];
|
|
3823
4991
|
return runGpuProcess({
|
|
@@ -3851,7 +5019,7 @@ async function runTransformersInference(args) {
|
|
|
3851
5019
|
}
|
|
3852
5020
|
let output;
|
|
3853
5021
|
try {
|
|
3854
|
-
output = JSON.parse(await
|
|
5022
|
+
output = JSON.parse(await readFile8(outputPath, "utf8"));
|
|
3855
5023
|
} catch (error) {
|
|
3856
5024
|
throw new Error(
|
|
3857
5025
|
`Transformers inference did not write valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -3868,7 +5036,7 @@ function assertEvaluationScoringReady(config) {
|
|
|
3868
5036
|
}
|
|
3869
5037
|
}
|
|
3870
5038
|
function baselineCacheKey(args) {
|
|
3871
|
-
return
|
|
5039
|
+
return createHash8("sha256").update(JSON.stringify({
|
|
3872
5040
|
v: 6,
|
|
3873
5041
|
inference_protocol_version: INFERENCE_PROTOCOL_VERSION,
|
|
3874
5042
|
package_version: args.packageVersion,
|
|
@@ -3888,15 +5056,15 @@ function baselineInputsAreStable(args) {
|
|
|
3888
5056
|
return args.config.paths.baseModel ? Boolean(args.sourceFingerprint) : Boolean(args.baseModelRevision);
|
|
3889
5057
|
}
|
|
3890
5058
|
function baselineStoreRoot(config) {
|
|
3891
|
-
return config.storeRoot ?
|
|
5059
|
+
return config.storeRoot ? resolve12(config.storeRoot) : defaultLocalHome();
|
|
3892
5060
|
}
|
|
3893
5061
|
function baselineCachePath(config, key) {
|
|
3894
|
-
return
|
|
5062
|
+
return join10(baselineStoreRoot(config), "cache", "baseline-evals", `${key}.json`);
|
|
3895
5063
|
}
|
|
3896
5064
|
async function readBaselineCache(config, path) {
|
|
3897
5065
|
await hardenExistingLocalStore(baselineStoreRoot(config));
|
|
3898
5066
|
try {
|
|
3899
|
-
return evalReportSchema.parse(JSON.parse(await
|
|
5067
|
+
return evalReportSchema.parse(JSON.parse(await readFile8(path, "utf8")));
|
|
3900
5068
|
} catch {
|
|
3901
5069
|
return null;
|
|
3902
5070
|
}
|
|
@@ -3905,7 +5073,7 @@ var cachedPackageVersion = null;
|
|
|
3905
5073
|
async function packageVersion() {
|
|
3906
5074
|
if (cachedPackageVersion !== null) return cachedPackageVersion;
|
|
3907
5075
|
try {
|
|
3908
|
-
const raw = await
|
|
5076
|
+
const raw = await readFile8(resolve12(localRuntimePackageRoot(import.meta.url), "package.json"), "utf8");
|
|
3909
5077
|
cachedPackageVersion = String(
|
|
3910
5078
|
JSON.parse(raw).version ?? "unknown"
|
|
3911
5079
|
);
|
|
@@ -3974,7 +5142,7 @@ async function evaluateExamples(args) {
|
|
|
3974
5142
|
baseModelId,
|
|
3975
5143
|
args.baseModelRevision
|
|
3976
5144
|
);
|
|
3977
|
-
const localModelSource = args.modelSource ?
|
|
5145
|
+
const localModelSource = args.modelSource ? resolve12(args.modelSource) !== resolve12(baseModelId) : false;
|
|
3978
5146
|
const baseModelRevision = localModelSource ? defaultBaseModelRevision(baseModelId) : requestedBaseModelRevision;
|
|
3979
5147
|
const maxExamples = args.config.evaluation.maxExamples ?? args.maxExamples ?? args.examples.length;
|
|
3980
5148
|
const truncated = args.examples.length > maxExamples;
|
|
@@ -4090,8 +5258,8 @@ function compareEvalReports(baseline, candidate) {
|
|
|
4090
5258
|
}
|
|
4091
5259
|
|
|
4092
5260
|
// src/local-runtime/process-training.ts
|
|
4093
|
-
import { access, mkdir as
|
|
4094
|
-
import { join as
|
|
5261
|
+
import { access, mkdir as mkdir10, readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
|
|
5262
|
+
import { join as join11, resolve as resolve13 } from "path";
|
|
4095
5263
|
function buildTrainingHyperparameters(request, options = {}) {
|
|
4096
5264
|
const model = resolveTrainingModel(request.spec_snapshot.base_model);
|
|
4097
5265
|
const hyper = request.hyperparameters;
|
|
@@ -4182,15 +5350,15 @@ function createTrainingProgressForwarder(reporter) {
|
|
|
4182
5350
|
async function launchProcessTraining(args) {
|
|
4183
5351
|
const { request, artifacts, config } = args;
|
|
4184
5352
|
const jobName = `tt-local-${request.run_id}`;
|
|
4185
|
-
await
|
|
5353
|
+
await mkdir10(artifacts.trainingDir, { recursive: true });
|
|
4186
5354
|
await copyDatasetToTrainingChannel(artifacts);
|
|
4187
5355
|
const hyperparameters = buildTrainingHyperparameters(request, {
|
|
4188
5356
|
baseModelRevision: args.baseModelRevision
|
|
4189
5357
|
});
|
|
4190
|
-
await writeJson(
|
|
5358
|
+
await writeJson(join11(artifacts.trainingConfigDir, "hyperparameters.json"), hyperparameters);
|
|
4191
5359
|
const entrypoint = buildBundledPythonCommand("train.py");
|
|
4192
5360
|
if (config.dryRun) {
|
|
4193
|
-
await
|
|
5361
|
+
await writeFile7(
|
|
4194
5362
|
artifacts.trainingLog,
|
|
4195
5363
|
"Dry run enabled. Training process was not launched.\n",
|
|
4196
5364
|
"utf8"
|
|
@@ -4212,11 +5380,11 @@ async function launchProcessTraining(args) {
|
|
|
4212
5380
|
withOfflineHuggingFaceCacheEnvironment({
|
|
4213
5381
|
...inheritedEnv,
|
|
4214
5382
|
BACKEND: "local",
|
|
4215
|
-
SM_CHANNEL_TRAINING:
|
|
4216
|
-
SM_CHANNEL_BASE_MODEL: config.paths.baseModel ?
|
|
4217
|
-
SM_MODEL_DIR:
|
|
4218
|
-
SM_OUTPUT_DIR:
|
|
4219
|
-
TT_HYPERPARAMETERS_PATH:
|
|
5383
|
+
SM_CHANNEL_TRAINING: resolve13(artifacts.trainingInputDir),
|
|
5384
|
+
SM_CHANNEL_BASE_MODEL: config.paths.baseModel ? resolve13(config.paths.baseModel) : void 0,
|
|
5385
|
+
SM_MODEL_DIR: resolve13(artifacts.trainingModelDir),
|
|
5386
|
+
SM_OUTPUT_DIR: resolve13(artifacts.trainingOutputDir),
|
|
5387
|
+
TT_HYPERPARAMETERS_PATH: resolve13(artifacts.trainingConfigDir, "hyperparameters.json")
|
|
4220
5388
|
}, config.paths.modelCache)
|
|
4221
5389
|
);
|
|
4222
5390
|
for (const [key, value] of Object.entries(childEnv)) {
|
|
@@ -4224,10 +5392,10 @@ async function launchProcessTraining(args) {
|
|
|
4224
5392
|
}
|
|
4225
5393
|
const remoteBaseModel = config.gpu ? gpuBaseModelPath(config, request.spec_snapshot.base_model, args.baseModelRevision) : void 0;
|
|
4226
5394
|
if (remoteBaseModel) childEnv.SM_CHANNEL_BASE_MODEL = remoteBaseModel;
|
|
4227
|
-
if (config.paths.modelCache) await
|
|
5395
|
+
if (config.paths.modelCache) await mkdir10(resolve13(config.paths.modelCache), { recursive: true });
|
|
4228
5396
|
if (config.paths.baseModel) {
|
|
4229
5397
|
await verifyLocalBaseModel(
|
|
4230
|
-
|
|
5398
|
+
resolve13(config.paths.baseModel),
|
|
4231
5399
|
request.spec_snapshot.base_model,
|
|
4232
5400
|
config.paths.modelCache
|
|
4233
5401
|
);
|
|
@@ -4266,12 +5434,12 @@ async function launchProcessTraining(args) {
|
|
|
4266
5434
|
onLine: (line) => forwardTrainingProgress(line),
|
|
4267
5435
|
shouldCancel: args.shouldCancel
|
|
4268
5436
|
});
|
|
4269
|
-
const metricsPath =
|
|
4270
|
-
const metrics = await pathExists(metricsPath) ? JSON.parse(await
|
|
5437
|
+
const metricsPath = join11(artifacts.trainingModelDir, "training-metrics.json");
|
|
5438
|
+
const metrics = await pathExists(metricsPath) ? JSON.parse(await readFile9(metricsPath, "utf8")) : null;
|
|
4271
5439
|
if (exitCode !== 0) {
|
|
4272
5440
|
throw new Error(`Training process exited with code ${exitCode}. See ${artifacts.trainingLog}.`);
|
|
4273
5441
|
}
|
|
4274
|
-
const modelTarPath =
|
|
5442
|
+
const modelTarPath = join11(artifacts.trainingOutputDir, "model.tar.gz");
|
|
4275
5443
|
const modelUri = await pathExists(modelTarPath) ? fileUri(modelTarPath) : fileUri(artifacts.trainingModelDir);
|
|
4276
5444
|
await args.reporter?.onEvent?.({
|
|
4277
5445
|
stage: "training",
|
|
@@ -4366,7 +5534,7 @@ function validateLocalPipeline(pipeline) {
|
|
|
4366
5534
|
return pipeline;
|
|
4367
5535
|
}
|
|
4368
5536
|
async function loadJsonFile(path) {
|
|
4369
|
-
return JSON.parse(await
|
|
5537
|
+
return JSON.parse(await readFile10(path, "utf8"));
|
|
4370
5538
|
}
|
|
4371
5539
|
async function loadRunRequest(path) {
|
|
4372
5540
|
return fineTuneRunRequestSchema.parse(await loadJsonFile(path));
|
|
@@ -4374,13 +5542,13 @@ async function loadRunRequest(path) {
|
|
|
4374
5542
|
function parseLocalRunnerConfig(document, path) {
|
|
4375
5543
|
const config = localRunnerConfigSchema.parse(document);
|
|
4376
5544
|
if (!path) return config;
|
|
4377
|
-
const configPath =
|
|
5545
|
+
const configPath = resolve14(path);
|
|
4378
5546
|
const base = dirname9(configPath);
|
|
4379
5547
|
const configPathValue = (value) => {
|
|
4380
5548
|
if (!value) return void 0;
|
|
4381
5549
|
if (value === "~") return homedir4();
|
|
4382
|
-
if (value.startsWith("~/")) return
|
|
4383
|
-
return
|
|
5550
|
+
if (value.startsWith("~/")) return resolve14(homedir4(), value.slice(2));
|
|
5551
|
+
return resolve14(base, value);
|
|
4384
5552
|
};
|
|
4385
5553
|
return {
|
|
4386
5554
|
...config,
|
|
@@ -4402,7 +5570,7 @@ function parseLocalRunnerConfig(document, path) {
|
|
|
4402
5570
|
}
|
|
4403
5571
|
async function loadLocalRunnerConfig(path) {
|
|
4404
5572
|
if (!path) return parseLocalRunnerConfig({});
|
|
4405
|
-
const configPath =
|
|
5573
|
+
const configPath = resolve14(path);
|
|
4406
5574
|
return parseLocalRunnerConfig(
|
|
4407
5575
|
await loadJsonFile(configPath),
|
|
4408
5576
|
configPath
|
|
@@ -4417,7 +5585,7 @@ function stripFileUri(path) {
|
|
|
4417
5585
|
}
|
|
4418
5586
|
async function pathExists2(path) {
|
|
4419
5587
|
try {
|
|
4420
|
-
await
|
|
5588
|
+
await stat9(path);
|
|
4421
5589
|
return true;
|
|
4422
5590
|
} catch {
|
|
4423
5591
|
return false;
|
|
@@ -4461,7 +5629,7 @@ function addDryRunPlaceholders(request, dryRun) {
|
|
|
4461
5629
|
};
|
|
4462
5630
|
}
|
|
4463
5631
|
async function validateDatasetInputs(request, config) {
|
|
4464
|
-
const inputIdentity =
|
|
5632
|
+
const inputIdentity = datasetInputIdentity;
|
|
4465
5633
|
const evalSampleSeed = config.evaluation.sampleSeed ?? deriveSampleSeed(request.behavior_spec_id);
|
|
4466
5634
|
if (!request.dataset_prebuilt) {
|
|
4467
5635
|
const inlineExamples = examplesFromSpec(request.spec_snapshot);
|
|
@@ -4551,7 +5719,10 @@ async function validateDatasetInputs(request, config) {
|
|
|
4551
5719
|
}
|
|
4552
5720
|
async function validateLocalFineTuneInput(input) {
|
|
4553
5721
|
const config = localRunnerConfigSchema.parse(input.config);
|
|
4554
|
-
const request =
|
|
5722
|
+
const request = await resolveRequestDataset(
|
|
5723
|
+
fineTuneRunRequestSchema.parse(addDryRunPlaceholders(input.request, config.dryRun)),
|
|
5724
|
+
config
|
|
5725
|
+
);
|
|
4555
5726
|
await validateDatasetInputs(request, config);
|
|
4556
5727
|
if (config.evaluation.generalRegression) {
|
|
4557
5728
|
await evaluationSuiteFromChatJsonl(
|
|
@@ -4568,14 +5739,14 @@ function artifactPrefix(request) {
|
|
|
4568
5739
|
runId: request.run_id
|
|
4569
5740
|
});
|
|
4570
5741
|
}
|
|
4571
|
-
function
|
|
5742
|
+
function canonicalJson5(value) {
|
|
4572
5743
|
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
4573
|
-
if (Array.isArray(value)) return `[${value.map((entry) =>
|
|
5744
|
+
if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson5(entry)).join(",")}]`;
|
|
4574
5745
|
const object = value;
|
|
4575
|
-
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${
|
|
5746
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson5(object[key])}`).join(",")}}`;
|
|
4576
5747
|
}
|
|
4577
5748
|
function hashJson(value) {
|
|
4578
|
-
return
|
|
5749
|
+
return createHash9("sha256").update(canonicalJson5(value)).digest("hex");
|
|
4579
5750
|
}
|
|
4580
5751
|
var packageRoot2 = localRuntimePackageRoot(import.meta.url);
|
|
4581
5752
|
async function hashFileIfPresent(path) {
|
|
@@ -4587,21 +5758,21 @@ async function hashFileIfPresent(path) {
|
|
|
4587
5758
|
}
|
|
4588
5759
|
async function packageVersion2() {
|
|
4589
5760
|
try {
|
|
4590
|
-
const parsed = JSON.parse(await
|
|
5761
|
+
const parsed = JSON.parse(await readFile10(resolve14(packageRoot2, "package.json"), "utf8"));
|
|
4591
5762
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
4592
5763
|
} catch {
|
|
4593
5764
|
return "unknown";
|
|
4594
5765
|
}
|
|
4595
5766
|
}
|
|
4596
5767
|
async function runtimeFingerprint() {
|
|
4597
|
-
const bundledProject2 =
|
|
5768
|
+
const bundledProject2 = resolve14(packageRoot2, "training/adapter");
|
|
4598
5769
|
return hashJson({
|
|
4599
5770
|
tt_local_version: await packageVersion2(),
|
|
4600
5771
|
node_version: process.version,
|
|
4601
5772
|
platform: process.platform,
|
|
4602
5773
|
architecture: process.arch,
|
|
4603
|
-
runner_pyproject: await hashFileIfPresent(
|
|
4604
|
-
runner_uv_lock: await hashFileIfPresent(
|
|
5774
|
+
runner_pyproject: await hashFileIfPresent(resolve14(bundledProject2, "pyproject.toml")),
|
|
5775
|
+
runner_uv_lock: await hashFileIfPresent(resolve14(bundledProject2, "uv.lock"))
|
|
4605
5776
|
});
|
|
4606
5777
|
}
|
|
4607
5778
|
function preparedSourceFingerprint(args) {
|
|
@@ -4615,8 +5786,8 @@ function preparedSourceFingerprint(args) {
|
|
|
4615
5786
|
});
|
|
4616
5787
|
}
|
|
4617
5788
|
async function hashFile(path) {
|
|
4618
|
-
const hash =
|
|
4619
|
-
for await (const chunk of
|
|
5789
|
+
const hash = createHash9("sha256");
|
|
5790
|
+
for await (const chunk of createReadStream5(path)) hash.update(chunk);
|
|
4620
5791
|
return hash.digest("hex");
|
|
4621
5792
|
}
|
|
4622
5793
|
async function datasetFingerprints(request) {
|
|
@@ -4640,7 +5811,7 @@ async function resolveBaseModelRevision(request, config) {
|
|
|
4640
5811
|
request.hyperparameters.base_model_revision
|
|
4641
5812
|
);
|
|
4642
5813
|
if (config.paths.baseModel) {
|
|
4643
|
-
return defaultBaseModelRevision(request.spec_snapshot.base_model);
|
|
5814
|
+
return isCertifiedTrainingModel(request.spec_snapshot.base_model) ? defaultBaseModelRevision(request.spec_snapshot.base_model) : requested;
|
|
4644
5815
|
}
|
|
4645
5816
|
if (requested) return requested;
|
|
4646
5817
|
if (!request.spec_snapshot.base_model.includes("/")) {
|
|
@@ -4648,9 +5819,9 @@ async function resolveBaseModelRevision(request, config) {
|
|
|
4648
5819
|
}
|
|
4649
5820
|
const repository = `models--${request.spec_snapshot.base_model.replaceAll("/", "--")}`;
|
|
4650
5821
|
const cacheEnvironment = withHuggingFaceCacheEnvironment(process.env, config.paths.modelCache);
|
|
4651
|
-
const refPath =
|
|
5822
|
+
const refPath = resolve14(cacheEnvironment.HF_HUB_CACHE, repository, "refs", "main");
|
|
4652
5823
|
try {
|
|
4653
|
-
const revision = (await
|
|
5824
|
+
const revision = (await readFile10(refPath, "utf8")).trim();
|
|
4654
5825
|
if (revision) return revision;
|
|
4655
5826
|
} catch {
|
|
4656
5827
|
}
|
|
@@ -4669,18 +5840,18 @@ async function fingerprintLocalBaseModel(uri, expectedModelId, modelCache) {
|
|
|
4669
5840
|
const files = [];
|
|
4670
5841
|
const visit = async (directory) => {
|
|
4671
5842
|
for (const entry of await readdir5(directory, { withFileTypes: true })) {
|
|
4672
|
-
const child =
|
|
5843
|
+
const child = resolve14(directory, entry.name);
|
|
4673
5844
|
if (entry.isDirectory()) {
|
|
4674
5845
|
await visit(child);
|
|
4675
5846
|
continue;
|
|
4676
5847
|
}
|
|
4677
|
-
const childMetadata = await
|
|
5848
|
+
const childMetadata = await stat9(child);
|
|
4678
5849
|
if (entry.isSymbolicLink() && !childMetadata.isFile()) {
|
|
4679
5850
|
throw new Error(`Local base model contains a non-file symbolic link: ${child}`);
|
|
4680
5851
|
}
|
|
4681
5852
|
if (!childMetadata.isFile()) continue;
|
|
4682
5853
|
files.push({
|
|
4683
|
-
path:
|
|
5854
|
+
path: relative5(root, child).split("\\").join("/"),
|
|
4684
5855
|
size_bytes: childMetadata.size,
|
|
4685
5856
|
sha256: await hashFile(child)
|
|
4686
5857
|
});
|
|
@@ -4709,26 +5880,26 @@ async function readStageMetadata(path) {
|
|
|
4709
5880
|
}
|
|
4710
5881
|
async function removePrefixedArtifacts(path) {
|
|
4711
5882
|
const directory = dirname9(path);
|
|
4712
|
-
const prefix = `${
|
|
5883
|
+
const prefix = `${basename3(path)}.`;
|
|
4713
5884
|
const names = await readdir5(directory).catch(() => []);
|
|
4714
5885
|
await Promise.all([
|
|
4715
|
-
|
|
4716
|
-
...names.filter((name) => name.startsWith(prefix)).map((name) =>
|
|
5886
|
+
rm5(path, { force: true }),
|
|
5887
|
+
...names.filter((name) => name.startsWith(prefix)).map((name) => rm5(resolve14(directory, name), { recursive: true, force: true }))
|
|
4717
5888
|
]);
|
|
4718
5889
|
}
|
|
4719
5890
|
async function cleanupStageArtifacts(artifacts, stage) {
|
|
4720
|
-
const removeReport = async () =>
|
|
5891
|
+
const removeReport = async () => rm5(artifacts.runReportJson, { force: true });
|
|
4721
5892
|
if (stage === "prepare") {
|
|
4722
5893
|
await Promise.all([
|
|
4723
5894
|
removePrefixedArtifacts(artifacts.baselineEvalJson),
|
|
4724
5895
|
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
4725
5896
|
removePrefixedArtifacts(artifacts.generalBaselineEvalJson),
|
|
4726
5897
|
removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
|
|
4727
|
-
|
|
5898
|
+
rm5(artifacts.trainingDir, { recursive: true, force: true }),
|
|
4728
5899
|
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
4729
|
-
|
|
5900
|
+
rm5(resolve14(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
4730
5901
|
removeReport(),
|
|
4731
|
-
|
|
5902
|
+
rm5(artifacts.artifactManifestJson, { force: true })
|
|
4732
5903
|
]);
|
|
4733
5904
|
return;
|
|
4734
5905
|
}
|
|
@@ -4742,9 +5913,9 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
4742
5913
|
}
|
|
4743
5914
|
if (stage === "train") {
|
|
4744
5915
|
await Promise.all([
|
|
4745
|
-
|
|
5916
|
+
rm5(artifacts.trainingDir, { recursive: true, force: true }),
|
|
4746
5917
|
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
4747
|
-
|
|
5918
|
+
rm5(resolve14(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
4748
5919
|
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
4749
5920
|
removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
|
|
4750
5921
|
removeReport()
|
|
@@ -4763,7 +5934,7 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
4763
5934
|
await removeReport();
|
|
4764
5935
|
}
|
|
4765
5936
|
function manifestRelativePath(artifacts, path) {
|
|
4766
|
-
return
|
|
5937
|
+
return relative5(artifacts.runDir, path).split("\\").join("/");
|
|
4767
5938
|
}
|
|
4768
5939
|
async function verifyReusableArtifacts(artifacts, paths, options = {}) {
|
|
4769
5940
|
if (!await pathExists2(artifacts.artifactManifestJson)) return false;
|
|
@@ -4780,11 +5951,11 @@ async function throwIfCancelled(store, request) {
|
|
|
4780
5951
|
}
|
|
4781
5952
|
}
|
|
4782
5953
|
async function acquireWorkflowLock(lockPath, description) {
|
|
4783
|
-
await
|
|
4784
|
-
const token =
|
|
5954
|
+
await mkdir11(dirname9(lockPath), { recursive: true });
|
|
5955
|
+
const token = randomUUID6();
|
|
4785
5956
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
4786
5957
|
try {
|
|
4787
|
-
const handle = await
|
|
5958
|
+
const handle = await open4(lockPath, "wx", 384);
|
|
4788
5959
|
try {
|
|
4789
5960
|
await handle.writeFile(`${JSON.stringify({ token, pid: process.pid, created_at: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
4790
5961
|
`);
|
|
@@ -4793,17 +5964,17 @@ async function acquireWorkflowLock(lockPath, description) {
|
|
|
4793
5964
|
}
|
|
4794
5965
|
return async () => {
|
|
4795
5966
|
try {
|
|
4796
|
-
const owner = JSON.parse(await
|
|
4797
|
-
if (owner.token === token) await
|
|
5967
|
+
const owner = JSON.parse(await readFile10(lockPath, "utf8"));
|
|
5968
|
+
if (owner.token === token) await rm5(lockPath, { force: true });
|
|
4798
5969
|
} catch {
|
|
4799
5970
|
}
|
|
4800
5971
|
};
|
|
4801
5972
|
} catch (error) {
|
|
4802
5973
|
if (error.code !== "EEXIST") throw error;
|
|
4803
|
-
const metadata = await
|
|
5974
|
+
const metadata = await stat9(lockPath).catch(() => null);
|
|
4804
5975
|
let owner = null;
|
|
4805
5976
|
try {
|
|
4806
|
-
owner = JSON.parse(await
|
|
5977
|
+
owner = JSON.parse(await readFile10(lockPath, "utf8"));
|
|
4807
5978
|
} catch {
|
|
4808
5979
|
owner = null;
|
|
4809
5980
|
}
|
|
@@ -4822,10 +5993,10 @@ async function acquireWorkflowLock(lockPath, description) {
|
|
|
4822
5993
|
`${description} already has an active local workflow (lock: ${lockPath}).`
|
|
4823
5994
|
);
|
|
4824
5995
|
}
|
|
4825
|
-
const stalePath = `${lockPath}.stale.${
|
|
5996
|
+
const stalePath = `${lockPath}.stale.${randomUUID6()}`;
|
|
4826
5997
|
try {
|
|
4827
|
-
await
|
|
4828
|
-
await
|
|
5998
|
+
await rename4(lockPath, stalePath);
|
|
5999
|
+
await rm5(stalePath, { force: true });
|
|
4829
6000
|
} catch (renameError) {
|
|
4830
6001
|
if (renameError.code !== "ENOENT") throw renameError;
|
|
4831
6002
|
}
|
|
@@ -4835,11 +6006,11 @@ async function acquireWorkflowLock(lockPath, description) {
|
|
|
4835
6006
|
}
|
|
4836
6007
|
async function acquireRunLock(store, runId) {
|
|
4837
6008
|
await store.ensure();
|
|
4838
|
-
return acquireWorkflowLock(
|
|
6009
|
+
return acquireWorkflowLock(resolve14(store.paths.runsDir, runId, "workflow.lock"), `Run ${runId}`);
|
|
4839
6010
|
}
|
|
4840
6011
|
async function acquireArtifactLock(artifacts) {
|
|
4841
6012
|
return acquireWorkflowLock(
|
|
4842
|
-
|
|
6013
|
+
join12(artifacts.runDir, ARTIFACT_WORKFLOW_LOCK_FILE),
|
|
4843
6014
|
`Artifact directory ${artifacts.runDir}`
|
|
4844
6015
|
);
|
|
4845
6016
|
}
|
|
@@ -4886,13 +6057,13 @@ function stageFingerprintPath(prepared, stage) {
|
|
|
4886
6057
|
return `${stageOutputPath(prepared, stage)}.stage.json`;
|
|
4887
6058
|
}
|
|
4888
6059
|
async function runnerFileFingerprints() {
|
|
4889
|
-
const project =
|
|
4890
|
-
const bundledSource =
|
|
4891
|
-
const pythonSources = (await readdir5(bundledSource, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".py")).map((entry) =>
|
|
6060
|
+
const project = resolve14(packageRoot2, "training/adapter");
|
|
6061
|
+
const bundledSource = resolve14(packageRoot2, "training/adapter/src");
|
|
6062
|
+
const pythonSources = (await readdir5(bundledSource, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".py")).map((entry) => resolve14(bundledSource, entry.name));
|
|
4892
6063
|
const candidates = [.../* @__PURE__ */ new Set([
|
|
4893
6064
|
...pythonSources,
|
|
4894
|
-
|
|
4895
|
-
|
|
6065
|
+
resolve14(project, "pyproject.toml"),
|
|
6066
|
+
resolve14(project, "uv.lock")
|
|
4896
6067
|
])];
|
|
4897
6068
|
const fingerprints = {};
|
|
4898
6069
|
for (const path of candidates.sort()) fingerprints[path] = await hashFileIfPresent(path);
|
|
@@ -5006,7 +6177,7 @@ async function ensureRunRecord(args) {
|
|
|
5006
6177
|
`Run ${args.request.run_id} cannot be reused with different user, behavior spec, or run number identity.`
|
|
5007
6178
|
);
|
|
5008
6179
|
}
|
|
5009
|
-
if (
|
|
6180
|
+
if (resolve14(existing.artifact_dir) !== resolve14(args.artifacts.runDir)) {
|
|
5010
6181
|
throw new Error(
|
|
5011
6182
|
`Run ${args.request.run_id} is already bound to artifact directory ${existing.artifact_dir}; it cannot be resumed with ${args.artifacts.runDir}.`
|
|
5012
6183
|
);
|
|
@@ -5084,7 +6255,7 @@ async function computePreparedRun(args) {
|
|
|
5084
6255
|
base_model_for_evaluation: baseModelForEvaluation,
|
|
5085
6256
|
base_model_revision: baseModelRevision ?? null,
|
|
5086
6257
|
base_model_fingerprint: baseModelFingerprint ?? null,
|
|
5087
|
-
system_prompt_sha256:
|
|
6258
|
+
system_prompt_sha256: createHash9("sha256").update(system).digest("hex"),
|
|
5088
6259
|
prepared_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5089
6260
|
};
|
|
5090
6261
|
if (args.writeArtifacts) await writeJsonAtomic(artifacts.stageMetadataJson, metadata);
|
|
@@ -5113,7 +6284,7 @@ async function prepareStage(args) {
|
|
|
5113
6284
|
const preparedExists = await pathExists2(args.artifacts.stageMetadataJson) && await pathExists2(args.artifacts.trainingJsonl);
|
|
5114
6285
|
const prepared = await computePreparedRun({ ...args, writeArtifacts: false });
|
|
5115
6286
|
await args.store.syncRunRequest(args.request, args.artifacts.runDir);
|
|
5116
|
-
await writeJsonAtomic(
|
|
6287
|
+
await writeJsonAtomic(resolve14(args.artifacts.runDir, "request.json"), args.request);
|
|
5117
6288
|
const existingMetadata = preparedExists ? await readStageMetadata(args.artifacts.stageMetadataJson) : null;
|
|
5118
6289
|
let canReuse = preparedExists && existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
|
|
5119
6290
|
if (canReuse) {
|
|
@@ -5447,6 +6618,56 @@ async function runCandidateStage(args) {
|
|
|
5447
6618
|
});
|
|
5448
6619
|
return report;
|
|
5449
6620
|
}
|
|
6621
|
+
async function buildRunProvenance(args) {
|
|
6622
|
+
const { metadata, request } = args.prepared;
|
|
6623
|
+
const gpu = args.config.gpu;
|
|
6624
|
+
return runProvenanceSchema.parse({
|
|
6625
|
+
schema_version: 1,
|
|
6626
|
+
recorded_at: args.recordedAt,
|
|
6627
|
+
software: {
|
|
6628
|
+
tt_version: await packageVersion2(),
|
|
6629
|
+
node_version: process.version,
|
|
6630
|
+
platform: process.platform,
|
|
6631
|
+
arch: process.arch,
|
|
6632
|
+
runtime_fingerprint: metadata.runtime_fingerprint,
|
|
6633
|
+
python_lock_sha256: await hashFileIfPresent(resolve14(packageRoot2, "training/adapter/uv.lock"))
|
|
6634
|
+
},
|
|
6635
|
+
execution: {
|
|
6636
|
+
target: gpu ? "aws" : "local",
|
|
6637
|
+
training_backend: args.training.provider,
|
|
6638
|
+
...gpu ? { instance_id: gpu.instanceId, ...gpu.region ? { region: gpu.region } : {} } : {}
|
|
6639
|
+
},
|
|
6640
|
+
spec: {
|
|
6641
|
+
behavior_spec_id: request.behavior_spec_id,
|
|
6642
|
+
run_number: request.run_number,
|
|
6643
|
+
snapshot_sha256: hashJson(request.spec_snapshot),
|
|
6644
|
+
system_prompt_sha256: metadata.system_prompt_sha256,
|
|
6645
|
+
...args.specFile ? { file_path: args.specFile.path, file_sha256: args.specFile.sha256 } : {}
|
|
6646
|
+
},
|
|
6647
|
+
base_model: {
|
|
6648
|
+
id: request.spec_snapshot.base_model,
|
|
6649
|
+
revision: metadata.base_model_revision,
|
|
6650
|
+
fingerprint: metadata.base_model_fingerprint
|
|
6651
|
+
},
|
|
6652
|
+
data: {
|
|
6653
|
+
prebuilt: metadata.dataset_prebuilt,
|
|
6654
|
+
format: metadata.dataset_format,
|
|
6655
|
+
file_sha256: metadata.dataset_fingerprints,
|
|
6656
|
+
compiled_training_sha256: await hashFileIfPresent(args.prepared.artifacts.trainingJsonl),
|
|
6657
|
+
training_examples: metadata.training_example_count,
|
|
6658
|
+
eval_split: metadata.eval_split,
|
|
6659
|
+
eval_sample_seed: metadata.eval_sample_seed,
|
|
6660
|
+
eval_examples_total: metadata.eval_examples_total,
|
|
6661
|
+
eval_examples_used: metadata.eval_examples_used
|
|
6662
|
+
},
|
|
6663
|
+
training: {
|
|
6664
|
+
hyperparameters: request.hyperparameters,
|
|
6665
|
+
exit_code: args.training.exit_code,
|
|
6666
|
+
log_uri: args.training.log_uri
|
|
6667
|
+
},
|
|
6668
|
+
source_fingerprint: metadata.source_fingerprint
|
|
6669
|
+
});
|
|
6670
|
+
}
|
|
5450
6671
|
async function runReportStage(args) {
|
|
5451
6672
|
if (!await pathExists2(args.prepared.artifacts.baselineEvalJson)) {
|
|
5452
6673
|
throw new Error("Run reporting requires baseline-eval.json.");
|
|
@@ -5539,6 +6760,13 @@ async function runReportStage(args) {
|
|
|
5539
6760
|
}
|
|
5540
6761
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5541
6762
|
const duration = elapsed(args.startedPerf);
|
|
6763
|
+
const provenance = await buildRunProvenance({
|
|
6764
|
+
prepared: args.prepared,
|
|
6765
|
+
config: args.config,
|
|
6766
|
+
training,
|
|
6767
|
+
recordedAt: completedAt,
|
|
6768
|
+
specFile: args.specFile
|
|
6769
|
+
});
|
|
5542
6770
|
const report = runReportSchema.parse({
|
|
5543
6771
|
run_id: args.prepared.request.run_id,
|
|
5544
6772
|
behavior_spec_id: args.prepared.request.behavior_spec_id,
|
|
@@ -5552,6 +6780,7 @@ async function runReportStage(args) {
|
|
|
5552
6780
|
comparison,
|
|
5553
6781
|
general_regression: generalRegression,
|
|
5554
6782
|
training,
|
|
6783
|
+
provenance,
|
|
5555
6784
|
artifact_uris: {
|
|
5556
6785
|
dataset: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
5557
6786
|
baseline_eval: fileUri(args.prepared.artifacts.baselineEvalJson),
|
|
@@ -5566,6 +6795,7 @@ async function runReportStage(args) {
|
|
|
5566
6795
|
dataset_prebuilt: args.prepared.metadata.dataset_prebuilt,
|
|
5567
6796
|
dataset_format: args.prepared.metadata.dataset_format,
|
|
5568
6797
|
dataset_uri: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
6798
|
+
...args.prepared.request.dataset_source ? { dataset_source: args.prepared.request.dataset_source } : {},
|
|
5569
6799
|
spec_example_count: args.prepared.request.spec_snapshot.examples.length,
|
|
5570
6800
|
training_example_count: args.prepared.metadata.training_example_count,
|
|
5571
6801
|
eval_examples_total: baseline.eval_examples_total,
|
|
@@ -5638,7 +6868,7 @@ async function runLocalPipeline(input) {
|
|
|
5638
6868
|
store,
|
|
5639
6869
|
reporter: input.reporter
|
|
5640
6870
|
});
|
|
5641
|
-
await writeJsonAtomic(
|
|
6871
|
+
await writeJsonAtomic(join12(artifacts.runDir, "resolved-workflow.json"), {
|
|
5642
6872
|
spec: input.projectSpec ?? {
|
|
5643
6873
|
...input.request.spec_snapshot,
|
|
5644
6874
|
hyperparameters: input.request.hyperparameters,
|
|
@@ -5660,7 +6890,7 @@ async function runLocalPipeline(input) {
|
|
|
5660
6890
|
if (step.uses === "evaluate") {
|
|
5661
6891
|
const isBase = step.with.model === "base";
|
|
5662
6892
|
const report = isBase ? await runBaselineStage({ prepared, config: input.config, store, reporter: input.reporter, runReporter }) : await runCandidateStage({ prepared, config: input.config, store, reporter: input.reporter, runReporter });
|
|
5663
|
-
const reportPath =
|
|
6893
|
+
const reportPath = join12(artifacts.runDir, "evaluations", `${step.id}.json`);
|
|
5664
6894
|
await writeJsonAtomic(reportPath, { ...report, artifact_uri: fileUri(reportPath) });
|
|
5665
6895
|
outputs[step.id] = { report: { ...report, artifact_uri: fileUri(reportPath) }, reportPath };
|
|
5666
6896
|
await refreshArtifactManifest(prepared);
|
|
@@ -5679,7 +6909,8 @@ async function runLocalPipeline(input) {
|
|
|
5679
6909
|
store,
|
|
5680
6910
|
reporter: input.reporter,
|
|
5681
6911
|
startedAt,
|
|
5682
|
-
startedPerf
|
|
6912
|
+
startedPerf,
|
|
6913
|
+
specFile: input.specFile
|
|
5683
6914
|
});
|
|
5684
6915
|
return { request: input.request, pipeline, status: "completed", outputs, report, reportPath: artifacts.runReportJson, artifactDir: artifacts.runDir };
|
|
5685
6916
|
}
|
|
@@ -5737,8 +6968,8 @@ async function runLocalFineTune(input) {
|
|
|
5737
6968
|
|
|
5738
6969
|
// src/local-runtime/project-workflow.ts
|
|
5739
6970
|
import { existsSync as existsSync3 } from "fs";
|
|
5740
|
-
import { readFile as
|
|
5741
|
-
import { dirname as dirname10, join as
|
|
6971
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6972
|
+
import { dirname as dirname10, join as join13, resolve as resolve15 } from "path";
|
|
5742
6973
|
function configForSpec(spec, specPath) {
|
|
5743
6974
|
return parseLocalRunnerConfig({
|
|
5744
6975
|
storeRoot: process.env.TT_LOCAL_HOME?.trim() ? expandUserPath(process.env.TT_LOCAL_HOME.trim()) : DEFAULT_PROJECT_STORE_ROOT,
|
|
@@ -5749,9 +6980,9 @@ function configForSpec(spec, specPath) {
|
|
|
5749
6980
|
async function resolveProjectConfig(specPath, configPath, warn, loaded) {
|
|
5750
6981
|
const input = loaded ?? (existsSync3(specPath) ? await loadLocalRunInput(specPath) : void 0);
|
|
5751
6982
|
const spec = input && input.kind !== "request" ? input.spec : void 0;
|
|
5752
|
-
const candidate = configPath ?
|
|
6983
|
+
const candidate = configPath ? resolve15(configPath) : join13(dirname10(resolve15(specPath)), "local-runner.json");
|
|
5753
6984
|
if (configPath || existsSync3(candidate)) {
|
|
5754
|
-
const legacy = parseLocalRunnerConfig(JSON.parse(await
|
|
6985
|
+
const legacy = parseLocalRunnerConfig(JSON.parse(await readFile11(candidate, "utf8")), candidate);
|
|
5755
6986
|
if (spec?.runtime || spec?.evaluation) {
|
|
5756
6987
|
throw new Error("Conflicting configuration sources: tunedtensor.json contains runtime/evaluation settings and a legacy runner config is present. Migrate to tunedtensor.json; do not silently override the spec.");
|
|
5757
6988
|
}
|
|
@@ -5881,6 +7112,9 @@ function trainingGpu(inventory) {
|
|
|
5881
7112
|
if (!listed.memory_total_bytes && inventory.python?.total_memory_bytes) {
|
|
5882
7113
|
return { ...listed, memory_total_bytes: inventory.python.total_memory_bytes };
|
|
5883
7114
|
}
|
|
7115
|
+
if (!listed.memory_total_bytes && (listed.unified_memory || isUnifiedGpuName(listed.name))) {
|
|
7116
|
+
return { ...listed, memory_total_bytes: inventory.os.total_memory_bytes, unified_memory: true };
|
|
7117
|
+
}
|
|
5884
7118
|
return listed;
|
|
5885
7119
|
}
|
|
5886
7120
|
if (inventory.python?.cuda_device) {
|
|
@@ -6026,6 +7260,12 @@ function formatCapabilitySummary(report) {
|
|
|
6026
7260
|
}
|
|
6027
7261
|
function warningsFromSnapshot(report, target) {
|
|
6028
7262
|
const warnings = [];
|
|
7263
|
+
if (target.engine === "decision") {
|
|
7264
|
+
if (!report.cuda_available) {
|
|
7265
|
+
warnings.push("No CUDA GPU detected: decision training runs on CPU or Apple MPS (roughly 15 s per 8-example step on a 4-core CPU).");
|
|
7266
|
+
}
|
|
7267
|
+
return warnings;
|
|
7268
|
+
}
|
|
6029
7269
|
if (target.engine === "foundation") {
|
|
6030
7270
|
for (const kind of ["train", "finetune"]) {
|
|
6031
7271
|
const verdict = report.foundation[kind];
|
|
@@ -6058,7 +7298,7 @@ function warningsFromSnapshot(report, target) {
|
|
|
6058
7298
|
import { constants as constants2 } from "fs";
|
|
6059
7299
|
import { access as access2, statfs } from "fs/promises";
|
|
6060
7300
|
import { cpus, freemem, homedir as homedir5, totalmem, type } from "os";
|
|
6061
|
-
import { dirname as dirname11, join as
|
|
7301
|
+
import { dirname as dirname11, join as join14, resolve as resolve16 } from "path";
|
|
6062
7302
|
import { spawn as spawn2 } from "child_process";
|
|
6063
7303
|
async function runCommand(command, args, options = {}) {
|
|
6064
7304
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
@@ -6212,7 +7452,7 @@ async function queryNvidiaGpus(env) {
|
|
|
6212
7452
|
return { result: table.code === 0 || !last ? table : last, gpus };
|
|
6213
7453
|
}
|
|
6214
7454
|
async function diskCheck(name, path) {
|
|
6215
|
-
const resolvedPath =
|
|
7455
|
+
const resolvedPath = resolve16(path);
|
|
6216
7456
|
try {
|
|
6217
7457
|
await access2(resolvedPath, constants2.R_OK);
|
|
6218
7458
|
const fs = await statfs(resolvedPath);
|
|
@@ -6313,10 +7553,10 @@ async function collectHostInventory(options = {}) {
|
|
|
6313
7553
|
const cwd = options.cwd ?? process.cwd();
|
|
6314
7554
|
const nodeVersion = process.versions.node;
|
|
6315
7555
|
const nodeMajor = Number(nodeVersion.split(".")[0]);
|
|
6316
|
-
const artifactRoot =
|
|
6317
|
-
const storeRoot =
|
|
6318
|
-
const modelCache =
|
|
6319
|
-
config?.paths.modelCache ?? env.HF_HOME ??
|
|
7556
|
+
const artifactRoot = resolve16(cwd, config?.artifactRoot ?? join14(cwd, DEFAULT_ARTIFACT_ROOT));
|
|
7557
|
+
const storeRoot = resolve16(config?.storeRoot ?? defaultStoreRoot(env));
|
|
7558
|
+
const modelCache = resolve16(
|
|
7559
|
+
config?.paths.modelCache ?? env.HF_HOME ?? join14(homedir5(), ".cache", "huggingface")
|
|
6320
7560
|
);
|
|
6321
7561
|
const [artifactDisk, storeDisk, cacheDisk, uvVersion, nvidiaQueried] = await Promise.all([
|
|
6322
7562
|
diskCheck("artifact-root", artifactRoot),
|
|
@@ -6362,17 +7602,17 @@ async function collectHostInventory(options = {}) {
|
|
|
6362
7602
|
}
|
|
6363
7603
|
|
|
6364
7604
|
// src/local-runtime/hardware-snapshot.ts
|
|
6365
|
-
import { chmod as chmod2, mkdir as
|
|
6366
|
-
import { dirname as dirname12, join as
|
|
7605
|
+
import { chmod as chmod2, mkdir as mkdir12, readFile as readFile12, writeFile as writeFile8 } from "fs/promises";
|
|
7606
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
6367
7607
|
var HARDWARE_SNAPSHOT_VERSION = 1;
|
|
6368
7608
|
var HARDWARE_SNAPSHOT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6369
7609
|
function hardwareSnapshotPath(env = process.env) {
|
|
6370
|
-
return
|
|
7610
|
+
return join15(getTunedTensorHome(env), "hardware.json");
|
|
6371
7611
|
}
|
|
6372
7612
|
async function readHardwareSnapshot(env = process.env) {
|
|
6373
7613
|
const path = hardwareSnapshotPath(env);
|
|
6374
7614
|
try {
|
|
6375
|
-
const parsed = JSON.parse(await
|
|
7615
|
+
const parsed = JSON.parse(await readFile12(path, "utf8"));
|
|
6376
7616
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
6377
7617
|
const snapshot = parsed;
|
|
6378
7618
|
if (snapshot.version !== HARDWARE_SNAPSHOT_VERSION) return void 0;
|
|
@@ -6389,7 +7629,7 @@ async function readHardwareSnapshot(env = process.env) {
|
|
|
6389
7629
|
}
|
|
6390
7630
|
async function writeHardwareSnapshot(snapshot, env = process.env) {
|
|
6391
7631
|
const path = hardwareSnapshotPath(env);
|
|
6392
|
-
await
|
|
7632
|
+
await mkdir12(dirname12(path), { recursive: true, mode: 448 });
|
|
6393
7633
|
const gpu = snapshot.capabilities.gpu;
|
|
6394
7634
|
const record = {
|
|
6395
7635
|
version: HARDWARE_SNAPSHOT_VERSION,
|
|
@@ -6400,7 +7640,7 @@ async function writeHardwareSnapshot(snapshot, env = process.env) {
|
|
|
6400
7640
|
capabilities: snapshot.capabilities,
|
|
6401
7641
|
summary: snapshot.summary ?? formatCapabilitySummary(snapshot.capabilities)
|
|
6402
7642
|
};
|
|
6403
|
-
await
|
|
7643
|
+
await writeFile8(path, `${JSON.stringify(record, null, 2)}
|
|
6404
7644
|
`, { mode: 384 });
|
|
6405
7645
|
await chmod2(path, 384);
|
|
6406
7646
|
return record;
|
|
@@ -6459,7 +7699,61 @@ async function assessHardware(options = {}) {
|
|
|
6459
7699
|
}, env);
|
|
6460
7700
|
}
|
|
6461
7701
|
|
|
7702
|
+
// src/local-runtime/console-reporter.ts
|
|
7703
|
+
function shortValue(value) {
|
|
7704
|
+
if (value === void 0 || value === null) return null;
|
|
7705
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
7706
|
+
return String(value);
|
|
7707
|
+
}
|
|
7708
|
+
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
7709
|
+
return null;
|
|
7710
|
+
}
|
|
7711
|
+
function formatRunEvent(event) {
|
|
7712
|
+
const detailText = Object.entries(event.details ?? {}).filter(([key]) => key !== "metrics").map(([key, value]) => {
|
|
7713
|
+
const formatted = key === "command" && Array.isArray(value) ? value.join(" ") : shortValue(value);
|
|
7714
|
+
return formatted ? `${key}=${formatted}` : null;
|
|
7715
|
+
}).filter((value) => Boolean(value)).slice(0, 5).join(" ");
|
|
7716
|
+
return sanitizeLogLine(`[tt] ${event.stage}: ${event.message}${detailText ? ` (${detailText})` : ""}`);
|
|
7717
|
+
}
|
|
7718
|
+
function elapsedLabel(milliseconds) {
|
|
7719
|
+
const total = Math.max(0, Math.floor(milliseconds / 1e3));
|
|
7720
|
+
const hours = Math.floor(total / 3600);
|
|
7721
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
7722
|
+
const seconds = total % 60;
|
|
7723
|
+
return `+${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
7724
|
+
}
|
|
7725
|
+
function createRunConsoleReporter(options = {}) {
|
|
7726
|
+
const write = options.write ?? ((text) => {
|
|
7727
|
+
process.stderr.write(text);
|
|
7728
|
+
});
|
|
7729
|
+
const now = options.now ?? (() => Date.now());
|
|
7730
|
+
const started = now();
|
|
7731
|
+
const stamp = (line) => line.replace(/^\[tt\] /, `[tt] ${elapsedLabel(now() - started)} `);
|
|
7732
|
+
const progress = createProgressRenderer({ write, tty: progressTty(), columns: progressColumns() });
|
|
7733
|
+
let lastLog = "";
|
|
7734
|
+
return {
|
|
7735
|
+
verbose: options.verbose ?? false,
|
|
7736
|
+
onProgress(update) {
|
|
7737
|
+
progress.update(update);
|
|
7738
|
+
},
|
|
7739
|
+
onEvent(event) {
|
|
7740
|
+
progress.interrupt();
|
|
7741
|
+
write(`${stamp(formatRunEvent(event))}
|
|
7742
|
+
`);
|
|
7743
|
+
},
|
|
7744
|
+
onLog(log) {
|
|
7745
|
+
const line = sanitizeLogLine(`[tt] ${log.stage}${log.stream ? ` ${log.stream}` : ""}: ${log.message}`);
|
|
7746
|
+
if (line === lastLog) return;
|
|
7747
|
+
lastLog = line;
|
|
7748
|
+
progress.interrupt();
|
|
7749
|
+
write(`${stamp(line)}
|
|
7750
|
+
`);
|
|
7751
|
+
}
|
|
7752
|
+
};
|
|
7753
|
+
}
|
|
7754
|
+
|
|
6462
7755
|
export {
|
|
7756
|
+
fileUri,
|
|
6463
7757
|
assertArtifactManifest,
|
|
6464
7758
|
TRAINING_MODELS,
|
|
6465
7759
|
resolveTrainingModel,
|
|
@@ -6467,9 +7761,12 @@ export {
|
|
|
6467
7761
|
defaultBaseModelRevision,
|
|
6468
7762
|
resolveModelLoader,
|
|
6469
7763
|
assertUsableModelArtifact,
|
|
7764
|
+
DECISION_MODELS,
|
|
7765
|
+
resolveDecisionModel,
|
|
6470
7766
|
DEFAULT_ARTIFACT_ROOT,
|
|
6471
7767
|
DEFAULT_PROJECT_STORE_ROOT,
|
|
6472
7768
|
DEFAULT_FOUNDATION_RUNS_DIR,
|
|
7769
|
+
DEFAULT_DECISION_RUNS_DIR,
|
|
6473
7770
|
expandUserPath,
|
|
6474
7771
|
getConfigDir,
|
|
6475
7772
|
getAgentConfigDir,
|
|
@@ -6481,7 +7778,12 @@ export {
|
|
|
6481
7778
|
behaviorSpecSchema,
|
|
6482
7779
|
specSnapshotSchema,
|
|
6483
7780
|
foundationHyperparametersSchema,
|
|
7781
|
+
huggingFaceRepoIdSchema,
|
|
7782
|
+
datasetColumnsSchema,
|
|
7783
|
+
huggingFaceDatasetSourceSchema,
|
|
7784
|
+
isRepoRelativePath,
|
|
6484
7785
|
datasetPrebuiltSchema,
|
|
7786
|
+
datasetSourceSchema,
|
|
6485
7787
|
fineTuneHyperparametersSchema,
|
|
6486
7788
|
fineTuneRunRequestSchema,
|
|
6487
7789
|
evalExampleResultSchema,
|
|
@@ -6490,13 +7792,27 @@ export {
|
|
|
6490
7792
|
evalReportSchema,
|
|
6491
7793
|
comparisonReportSchema,
|
|
6492
7794
|
trainingReportSchema,
|
|
7795
|
+
runProvenanceSchema,
|
|
6493
7796
|
runReportSchema,
|
|
6494
7797
|
localRunnerConfigSchema,
|
|
6495
7798
|
projectRuntimeSchema,
|
|
6496
7799
|
localFoundationSpecFileSchema,
|
|
6497
7800
|
localAdapterSpecFileSchema,
|
|
7801
|
+
decisionQuestionSchema,
|
|
7802
|
+
decisionHyperparametersSchema,
|
|
7803
|
+
localDecisionSpecFileSchema,
|
|
7804
|
+
decisionLabels,
|
|
6498
7805
|
localBehaviorSpecFileSchema,
|
|
6499
7806
|
isFoundationSpecFile,
|
|
7807
|
+
isDecisionSpecFile,
|
|
7808
|
+
normalizeChatJsonlForRelocation,
|
|
7809
|
+
datasetInputIdentity,
|
|
7810
|
+
buildSystemMessage,
|
|
7811
|
+
exampleToChatRow,
|
|
7812
|
+
compileSpecToJsonl,
|
|
7813
|
+
examplesFromSpec,
|
|
7814
|
+
examplesFromChatJsonl,
|
|
7815
|
+
evaluationSuiteFromChatJsonl,
|
|
6500
7816
|
validateEvalCases,
|
|
6501
7817
|
DEFAULT_LOCAL_SPEC_PATH,
|
|
6502
7818
|
localPathsOverlap,
|
|
@@ -6506,6 +7822,7 @@ export {
|
|
|
6506
7822
|
generatedPlaceholderIssues,
|
|
6507
7823
|
assertLocalRunInputReady,
|
|
6508
7824
|
foundationPlaceholderIssues,
|
|
7825
|
+
decisionPlaceholderIssues,
|
|
6509
7826
|
assertFoundationSpecReady,
|
|
6510
7827
|
runRequestFromLocalSpec,
|
|
6511
7828
|
parseLocalRunInput,
|
|
@@ -6522,17 +7839,12 @@ export {
|
|
|
6522
7839
|
canonicalJson2 as canonicalJson,
|
|
6523
7840
|
isFoundationPipeline3 as isFoundationPipeline,
|
|
6524
7841
|
parsePipeline3 as parsePipeline,
|
|
6525
|
-
normalizeChatJsonlForRelocation,
|
|
6526
|
-
buildSystemMessage,
|
|
6527
|
-
exampleToChatRow,
|
|
6528
|
-
compileSpecToJsonl,
|
|
6529
|
-
examplesFromSpec,
|
|
6530
|
-
examplesFromChatJsonl,
|
|
6531
|
-
evaluationSuiteFromChatJsonl,
|
|
6532
7842
|
reportInBackground,
|
|
6533
7843
|
sanitizeLogLine,
|
|
6534
7844
|
forwardStreamLines,
|
|
6535
7845
|
localRuntimePackageRoot,
|
|
7846
|
+
buildDecisionPythonCommand,
|
|
7847
|
+
withDecisionPythonEnvironment,
|
|
6536
7848
|
buildBundledPythonCommand,
|
|
6537
7849
|
withBundledPythonEnvironment,
|
|
6538
7850
|
buildFoundationPythonCommand,
|
|
@@ -6543,6 +7855,7 @@ export {
|
|
|
6543
7855
|
withOfflineHuggingFaceCacheEnvironment,
|
|
6544
7856
|
checkAwsGpu,
|
|
6545
7857
|
runGpuProcess,
|
|
7858
|
+
auditRun,
|
|
6546
7859
|
isTerminalRunState,
|
|
6547
7860
|
defaultLocalHome,
|
|
6548
7861
|
localStorePaths,
|
|
@@ -6550,10 +7863,18 @@ export {
|
|
|
6550
7863
|
writePrivateJsonAtomic,
|
|
6551
7864
|
createLocalStore,
|
|
6552
7865
|
tokenF1,
|
|
7866
|
+
deriveSampleSeed,
|
|
7867
|
+
splitSpecExamples,
|
|
7868
|
+
compareEvalReports,
|
|
6553
7869
|
verifyModelPrefetchCacheReport,
|
|
6554
7870
|
buildModelPrefetchPayload,
|
|
7871
|
+
safeName,
|
|
6555
7872
|
verifyLocalBaseModel,
|
|
6556
7873
|
prefetchBaseModel,
|
|
7874
|
+
DatasetNotDownloadedError,
|
|
7875
|
+
huggingFaceDataset,
|
|
7876
|
+
prefetchHuggingFaceDataset,
|
|
7877
|
+
ensureDatasetCached,
|
|
6557
7878
|
evaluateGeneralRegressionGate,
|
|
6558
7879
|
canonicalLocalPipeline,
|
|
6559
7880
|
validateLocalPipeline,
|
|
@@ -6567,10 +7888,12 @@ export {
|
|
|
6567
7888
|
runLocalFineTune,
|
|
6568
7889
|
configForSpec,
|
|
6569
7890
|
resolveProjectConfig,
|
|
7891
|
+
evaluateCapabilities,
|
|
6570
7892
|
warningsFromSnapshot,
|
|
6571
7893
|
readHardwareSnapshot,
|
|
6572
7894
|
formatHostStatusLine,
|
|
6573
7895
|
formatAgentHostBlock,
|
|
6574
|
-
assessHardware
|
|
7896
|
+
assessHardware,
|
|
7897
|
+
createRunConsoleReporter
|
|
6575
7898
|
};
|
|
6576
|
-
//# sourceMappingURL=chunk-
|
|
7899
|
+
//# sourceMappingURL=chunk-4SUXBHIY.js.map
|