@aws/ml-container-creator 0.9.1 → 0.10.3
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/LICENSE-THIRD-PARTY +9304 -0
- package/bin/cli.js +2 -0
- package/config/bootstrap-e2e-stack.json +341 -0
- package/config/bootstrap-stack.json +40 -3
- package/config/parameter-schema-v2.json +2049 -0
- package/config/tune-catalog.json +1781 -0
- package/infra/ci-harness/buildspec.yml +1 -0
- package/infra/ci-harness/lambda/path-prover/brain.ts +306 -0
- package/infra/ci-harness/lambda/path-prover/write-results.ts +152 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +837 -7
- package/infra/ci-harness/state-machines/path-prover.asl.json +496 -0
- package/package.json +53 -68
- package/servers/base-image-picker/index.js +121 -121
- package/servers/e2e-status/index.js +297 -0
- package/servers/e2e-status/manifest.json +14 -0
- package/servers/e2e-status/package.json +15 -0
- package/servers/endpoint-picker/LICENSE +202 -0
- package/servers/endpoint-picker/index.js +536 -0
- package/servers/endpoint-picker/manifest.json +14 -0
- package/servers/endpoint-picker/package.json +18 -0
- package/servers/hyperpod-cluster-picker/index.js +125 -125
- package/servers/instance-sizer/index.js +138 -138
- package/servers/instance-sizer/lib/instance-ranker.js +76 -76
- package/servers/instance-sizer/lib/model-resolver.js +61 -61
- package/servers/instance-sizer/lib/quota-resolver.js +113 -113
- package/servers/instance-sizer/lib/vram-estimator.js +31 -31
- package/servers/lib/bedrock-client.js +38 -38
- package/servers/lib/catalogs/jumpstart-public.json +101 -16
- package/servers/lib/catalogs/model-servers.json +201 -3
- package/servers/lib/catalogs/models.json +182 -26
- package/servers/lib/custom-validators.js +13 -13
- package/servers/lib/dynamic-resolver.js +4 -4
- package/servers/marketplace-picker/index.js +342 -0
- package/servers/marketplace-picker/manifest.json +14 -0
- package/servers/marketplace-picker/package.json +18 -0
- package/servers/model-picker/index.js +382 -382
- package/servers/region-picker/index.js +56 -56
- package/servers/workload-picker/LICENSE +202 -0
- package/servers/workload-picker/catalogs/workload-profiles.json +67 -0
- package/servers/workload-picker/index.js +171 -0
- package/servers/workload-picker/manifest.json +16 -0
- package/servers/workload-picker/package.json +16 -0
- package/src/app.js +4 -390
- package/src/lib/bootstrap-command-handler.js +710 -1148
- package/src/lib/bootstrap-config.js +36 -0
- package/src/lib/bootstrap-profile-manager.js +641 -0
- package/src/lib/bootstrap-provisioners.js +421 -0
- package/src/lib/ci-register-helpers.js +74 -0
- package/src/lib/config-loader.js +408 -0
- package/src/lib/config-manager.js +66 -1685
- package/src/lib/config-mcp-client.js +118 -0
- package/src/lib/config-validator.js +634 -0
- package/src/lib/cuda-resolver.js +149 -0
- package/src/lib/e2e-catalog-validator.js +251 -3
- package/src/lib/e2e-ci-recorder.js +103 -0
- package/src/lib/generated/cli-options.js +315 -311
- package/src/lib/generated/parameter-matrix.js +671 -0
- package/src/lib/generated/validation-rules.js +71 -71
- package/src/lib/marketplace-flow.js +276 -0
- package/src/lib/mcp-query-runner.js +768 -0
- package/src/lib/parameter-schema-validator.js +62 -18
- package/src/lib/path-prover-brain.js +607 -0
- package/src/lib/prompt-runner.js +41 -1504
- package/src/lib/prompts/feature-prompts.js +172 -0
- package/src/lib/prompts/index.js +48 -0
- package/src/lib/prompts/infrastructure-prompts.js +690 -0
- package/src/lib/prompts/model-prompts.js +552 -0
- package/src/lib/prompts/project-prompts.js +82 -0
- package/src/lib/prompts.js +2 -1446
- package/src/lib/registry-command-handler.js +135 -3
- package/src/lib/secrets-prompt-runner.js +251 -0
- package/src/lib/template-variable-resolver.js +422 -0
- package/src/lib/tune-catalog-validator.js +37 -4
- package/templates/Dockerfile +9 -0
- package/templates/code/adapter_sidecar.py +444 -0
- package/templates/code/serve +6 -0
- package/templates/code/serve.d/vllm.ejs +1 -1
- package/templates/do/.benchmark_writer.py +1476 -0
- package/templates/do/.tune_helper.py +982 -57
- package/templates/do/__pycache__/.benchmark_writer.cpython-312.pyc +0 -0
- package/templates/do/adapter +149 -0
- package/templates/do/benchmark +639 -85
- package/templates/do/config +108 -5
- package/templates/do/deploy.d/managed-inference.ejs +192 -11
- package/templates/do/optimize +106 -37
- package/templates/do/register +89 -0
- package/templates/do/test +13 -0
- package/templates/do/tune +378 -59
- package/templates/do/validate +44 -4
- package/config/parameter-schema.json +0 -88
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Path Prover Brain
|
|
9
|
+
*
|
|
10
|
+
* Implements the intelligence layer for the Path Prover agent mode.
|
|
11
|
+
* This module identifies coverage gaps, finds nearest substitutions,
|
|
12
|
+
* classifies failures, gates tune/adapter stages, and builds
|
|
13
|
+
* Athena-compatible records with run_type='path_prove'.
|
|
14
|
+
*
|
|
15
|
+
* Feature: ci-benchmark-pipeline
|
|
16
|
+
* Requirements: 8.1–8.12
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ── Configuration Dimensions ─────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The ordered vector of config dimensions used for Hamming distance calculation.
|
|
23
|
+
*/
|
|
24
|
+
export const CONFIG_DIMENSIONS = [
|
|
25
|
+
'deployment_config',
|
|
26
|
+
'model_family',
|
|
27
|
+
'instance_family',
|
|
28
|
+
'quantization',
|
|
29
|
+
'tp_degree',
|
|
30
|
+
'deployment_target'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ── Failure Classification ───────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Valid failure categories for Path Prover classification.
|
|
37
|
+
*/
|
|
38
|
+
export const FAILURE_CATEGORIES = [
|
|
39
|
+
'capacity',
|
|
40
|
+
'timeout',
|
|
41
|
+
'oom',
|
|
42
|
+
'code_bug',
|
|
43
|
+
'model_incompatibility',
|
|
44
|
+
'service_limitation'
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Error pattern matchers for failure classification.
|
|
49
|
+
* Each entry maps a regex pattern to a category and retryable flag.
|
|
50
|
+
*/
|
|
51
|
+
const ERROR_PATTERNS = [
|
|
52
|
+
{ pattern: /InsufficientInstanceCapacity/i, category: 'capacity', retryable: true },
|
|
53
|
+
{ pattern: /CapacityError/i, category: 'capacity', retryable: true },
|
|
54
|
+
{ pattern: /no capacity/i, category: 'capacity', retryable: true },
|
|
55
|
+
{ pattern: /timed?\s*out/i, category: 'timeout', retryable: true },
|
|
56
|
+
{ pattern: /timeout/i, category: 'timeout', retryable: true },
|
|
57
|
+
{ pattern: /deadline exceeded/i, category: 'timeout', retryable: true },
|
|
58
|
+
{ pattern: /OutOfMemory/i, category: 'oom', retryable: false },
|
|
59
|
+
{ pattern: /OOM/i, category: 'oom', retryable: false },
|
|
60
|
+
{ pattern: /CUDA out of memory/i, category: 'oom', retryable: false },
|
|
61
|
+
{ pattern: /Cannot allocate memory/i, category: 'oom', retryable: false },
|
|
62
|
+
{ pattern: /killed.*memory/i, category: 'oom', retryable: false },
|
|
63
|
+
{ pattern: /template.*error/i, category: 'code_bug', retryable: false },
|
|
64
|
+
{ pattern: /SyntaxError/i, category: 'code_bug', retryable: false },
|
|
65
|
+
{ pattern: /ReferenceError/i, category: 'code_bug', retryable: false },
|
|
66
|
+
{ pattern: /TypeError/i, category: 'code_bug', retryable: false },
|
|
67
|
+
{ pattern: /script crash/i, category: 'code_bug', retryable: false },
|
|
68
|
+
{ pattern: /rendering failed/i, category: 'code_bug', retryable: false },
|
|
69
|
+
{ pattern: /not supported.*model/i, category: 'model_incompatibility', retryable: false },
|
|
70
|
+
{ pattern: /model.*incompatible/i, category: 'model_incompatibility', retryable: false },
|
|
71
|
+
{ pattern: /unsupported.*architecture/i, category: 'model_incompatibility', retryable: false },
|
|
72
|
+
{ pattern: /LoRA.*not supported/i, category: 'model_incompatibility', retryable: false },
|
|
73
|
+
{ pattern: /adapter.*not compatible/i, category: 'model_incompatibility', retryable: false },
|
|
74
|
+
{ pattern: /not available.*region/i, category: 'service_limitation', retryable: false },
|
|
75
|
+
{ pattern: /service.*not supported/i, category: 'service_limitation', retryable: false },
|
|
76
|
+
{ pattern: /API.*not available/i, category: 'service_limitation', retryable: false },
|
|
77
|
+
{ pattern: /feature.*not.*region/i, category: 'service_limitation', retryable: false },
|
|
78
|
+
{ pattern: /ValidationException/i, category: 'service_limitation', retryable: false }
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// ── Gap Identification (Task 5.1) ────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Identify coverage gaps given a set of proven configurations.
|
|
85
|
+
*
|
|
86
|
+
* A "gap" is a config dimension combination that has no records in Athena.
|
|
87
|
+
* This function compares the known dimension space (all unique values seen
|
|
88
|
+
* across proven configs) against what is actually proven, and returns
|
|
89
|
+
* combinations that are missing.
|
|
90
|
+
*
|
|
91
|
+
* @param {object[]} provenConfigs - Array of proven config objects from Athena
|
|
92
|
+
* Each object must have keys matching CONFIG_DIMENSIONS plus `status`
|
|
93
|
+
* @returns {object[]} Ordered list of gap configs to prove, sorted by
|
|
94
|
+
* coverage priority (more neighbors proven = higher priority)
|
|
95
|
+
*/
|
|
96
|
+
export function identifyGaps(provenConfigs) {
|
|
97
|
+
if (!provenConfigs || provenConfigs.length === 0) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Extract unique values for each dimension from proven configs
|
|
102
|
+
const dimensionValues = {};
|
|
103
|
+
for (const dim of CONFIG_DIMENSIONS) {
|
|
104
|
+
const values = new Set();
|
|
105
|
+
for (const config of provenConfigs) {
|
|
106
|
+
if (config[dim] !== undefined && config[dim] !== null) {
|
|
107
|
+
values.add(String(config[dim]));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
dimensionValues[dim] = [...values];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Build a set of proven config signatures for fast lookup
|
|
114
|
+
const provenSignatures = new Set();
|
|
115
|
+
for (const config of provenConfigs) {
|
|
116
|
+
if (config.status === 'completed') {
|
|
117
|
+
const sig = CONFIG_DIMENSIONS.map(d => String(config[d] ?? '')).join('|');
|
|
118
|
+
provenSignatures.add(sig);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generate all combinations from observed values and find gaps
|
|
123
|
+
const gaps = [];
|
|
124
|
+
const combinations = cartesianProduct(dimensionValues);
|
|
125
|
+
|
|
126
|
+
for (const combo of combinations) {
|
|
127
|
+
const sig = CONFIG_DIMENSIONS.map(d => String(combo[d] ?? '')).join('|');
|
|
128
|
+
if (!provenSignatures.has(sig)) {
|
|
129
|
+
// Count how many neighbors (distance=1) are proven — higher = more valuable
|
|
130
|
+
let neighborCount = 0;
|
|
131
|
+
for (const provenSig of provenSignatures) {
|
|
132
|
+
const provenParts = provenSig.split('|');
|
|
133
|
+
const comboParts = sig.split('|');
|
|
134
|
+
let diff = 0;
|
|
135
|
+
for (let i = 0; i < provenParts.length; i++) {
|
|
136
|
+
if (provenParts[i] !== comboParts[i]) diff++;
|
|
137
|
+
}
|
|
138
|
+
if (diff === 1) neighborCount++;
|
|
139
|
+
}
|
|
140
|
+
gaps.push({ ...combo, _neighborCount: neighborCount });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Sort by neighbor count descending (most surrounded gaps first)
|
|
145
|
+
gaps.sort((a, b) => b._neighborCount - a._neighborCount);
|
|
146
|
+
|
|
147
|
+
// Remove internal sorting field before returning
|
|
148
|
+
return gaps.map(({ _neighborCount, ...config }) => config);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate cartesian product of dimension value arrays.
|
|
153
|
+
* @param {object} dimensionValues - Map of dimension name to array of values
|
|
154
|
+
* @returns {object[]} Array of config objects representing all combinations
|
|
155
|
+
*/
|
|
156
|
+
function cartesianProduct(dimensionValues) {
|
|
157
|
+
const dims = CONFIG_DIMENSIONS;
|
|
158
|
+
const results = [];
|
|
159
|
+
|
|
160
|
+
function generate(index, current) {
|
|
161
|
+
if (index === dims.length) {
|
|
162
|
+
results.push({ ...current });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const dim = dims[index];
|
|
166
|
+
const values = dimensionValues[dim] || [];
|
|
167
|
+
if (values.length === 0) {
|
|
168
|
+
generate(index + 1, current);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const val of values) {
|
|
172
|
+
current[dim] = val;
|
|
173
|
+
generate(index + 1, current);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
generate(0, {});
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Substitution Algorithm (Task 5.2) ────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Find the nearest proven substitution for a requested configuration.
|
|
185
|
+
*
|
|
186
|
+
* Uses Hamming distance on the config dimension vector. Only considers
|
|
187
|
+
* configs with status='completed'. Never crosses the model_family boundary.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} requestedConfig - The requested config with dimension fields
|
|
190
|
+
* @param {object[]} provenConfigs - Array of proven configs from Athena
|
|
191
|
+
* @returns {object} Result object:
|
|
192
|
+
* - If matches found: { substitutions: [{config, distance, explanation}...] } (top 3)
|
|
193
|
+
* - If no matches: { noMatch: true, message: string }
|
|
194
|
+
*/
|
|
195
|
+
export function findNearestSubstitution(requestedConfig, provenConfigs) {
|
|
196
|
+
if (!requestedConfig || !provenConfigs || provenConfigs.length === 0) {
|
|
197
|
+
return { noMatch: true, message: 'no coverage — no proven configs available' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const requestedFamily = requestedConfig.model_family;
|
|
201
|
+
|
|
202
|
+
// Filter to only completed configs in the same model_family
|
|
203
|
+
const candidates = provenConfigs.filter(c =>
|
|
204
|
+
c.status === 'completed' && c.model_family === requestedFamily
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (candidates.length === 0) {
|
|
208
|
+
// Find nearest across families for the message
|
|
209
|
+
const allCompleted = provenConfigs.filter(c => c.status === 'completed');
|
|
210
|
+
if (allCompleted.length === 0) {
|
|
211
|
+
return { noMatch: true, message: 'no coverage — no proven configs available' };
|
|
212
|
+
}
|
|
213
|
+
const minDistance = Math.min(
|
|
214
|
+
...allCompleted.map(c => hammingDistance(requestedConfig, c))
|
|
215
|
+
);
|
|
216
|
+
return {
|
|
217
|
+
noMatch: true,
|
|
218
|
+
message: `no coverage — nearest proven config is ${minDistance} dimensions away`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Compute distances and sort
|
|
223
|
+
const scored = candidates.map(config => {
|
|
224
|
+
const distance = hammingDistance(requestedConfig, config);
|
|
225
|
+
const explanation = buildExplanation(requestedConfig, config);
|
|
226
|
+
return { config, distance, explanation };
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Sort by distance ascending, then by recency (if run_timestamp available)
|
|
230
|
+
scored.sort((a, b) => {
|
|
231
|
+
if (a.distance !== b.distance) return a.distance - b.distance;
|
|
232
|
+
// Secondary sort: prefer more recent configs
|
|
233
|
+
const aTime = a.config.run_timestamp || '';
|
|
234
|
+
const bTime = b.config.run_timestamp || '';
|
|
235
|
+
return bTime.localeCompare(aTime);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Return top 3
|
|
239
|
+
const substitutions = scored.slice(0, 3).map(({ config, distance, explanation }) => ({
|
|
240
|
+
config,
|
|
241
|
+
distance,
|
|
242
|
+
explanation
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
return { substitutions };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Compute Hamming distance between two config vectors.
|
|
250
|
+
* Counts the number of dimensions that differ.
|
|
251
|
+
*
|
|
252
|
+
* @param {object} configA - First config
|
|
253
|
+
* @param {object} configB - Second config
|
|
254
|
+
* @returns {number} Number of dimensions that differ
|
|
255
|
+
*/
|
|
256
|
+
export function hammingDistance(configA, configB) {
|
|
257
|
+
let distance = 0;
|
|
258
|
+
for (const dim of CONFIG_DIMENSIONS) {
|
|
259
|
+
const valA = String(configA[dim] ?? '');
|
|
260
|
+
const valB = String(configB[dim] ?? '');
|
|
261
|
+
if (valA !== valB) {
|
|
262
|
+
distance++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return distance;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Build a human-readable explanation of which dimensions differ.
|
|
270
|
+
*
|
|
271
|
+
* @param {object} requested - The requested config
|
|
272
|
+
* @param {object} suggested - The suggested substitution
|
|
273
|
+
* @returns {string[]} Array of dimension difference explanations
|
|
274
|
+
*/
|
|
275
|
+
function buildExplanation(requested, suggested) {
|
|
276
|
+
const diffs = [];
|
|
277
|
+
for (const dim of CONFIG_DIMENSIONS) {
|
|
278
|
+
const reqVal = String(requested[dim] ?? '');
|
|
279
|
+
const sugVal = String(suggested[dim] ?? '');
|
|
280
|
+
if (reqVal !== sugVal) {
|
|
281
|
+
diffs.push(`${dim}: '${reqVal}' → '${sugVal}'`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return diffs;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Tune/Adapter Stage Gating (Task 5.3) ─────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Determine whether tune/adapter stages should execute for a prove request.
|
|
291
|
+
*
|
|
292
|
+
* Tune stages only execute when the prove request explicitly includes
|
|
293
|
+
* fine-tuning (e.g., the gap involves a tune technique or the user
|
|
294
|
+
* requested adapter serving).
|
|
295
|
+
*
|
|
296
|
+
* @param {object} proveRequest - The prove request object
|
|
297
|
+
* @param {boolean} [proveRequest.include_tuning] - Explicitly request tuning
|
|
298
|
+
* @param {boolean} [proveRequest.enable_lora] - Whether LoRA is enabled
|
|
299
|
+
* @param {string} [proveRequest.tune_technique] - Tune technique (sft, dpo, etc.)
|
|
300
|
+
* @returns {boolean} True if tune stages should execute
|
|
301
|
+
*/
|
|
302
|
+
export function shouldExecuteTuneStages(proveRequest) {
|
|
303
|
+
if (!proveRequest) return false;
|
|
304
|
+
|
|
305
|
+
// Explicit tuning request
|
|
306
|
+
if (proveRequest.include_tuning === true) return true;
|
|
307
|
+
|
|
308
|
+
// LoRA adapter serving requested
|
|
309
|
+
if (proveRequest.enable_lora === true) return true;
|
|
310
|
+
|
|
311
|
+
// Tune technique specified
|
|
312
|
+
if (proveRequest.tune_technique && proveRequest.tune_technique !== 'none') return true;
|
|
313
|
+
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Failure Classification (Task 5.4) ────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Classify a failure from error output.
|
|
321
|
+
*
|
|
322
|
+
* Parses error output for known patterns and returns a structured
|
|
323
|
+
* classification with stage, category, and retryable flag.
|
|
324
|
+
*
|
|
325
|
+
* @param {string|object} errorOutput - Error output (string or structured object)
|
|
326
|
+
* @param {string} [errorOutput.error] - Error message (if object)
|
|
327
|
+
* @param {string} [errorOutput.stage] - Stage that failed (if object)
|
|
328
|
+
* @returns {object} Classification: { stage, category, retryable }
|
|
329
|
+
*/
|
|
330
|
+
export function classifyFailure(errorOutput) {
|
|
331
|
+
if (!errorOutput) {
|
|
332
|
+
return { stage: 'unknown', category: 'code_bug', retryable: false };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Extract error message and stage
|
|
336
|
+
let errorMsg = '';
|
|
337
|
+
let stage = 'unknown';
|
|
338
|
+
|
|
339
|
+
if (typeof errorOutput === 'string') {
|
|
340
|
+
errorMsg = errorOutput;
|
|
341
|
+
stage = detectStage(errorOutput);
|
|
342
|
+
} else if (typeof errorOutput === 'object') {
|
|
343
|
+
errorMsg = errorOutput.error || errorOutput.message || JSON.stringify(errorOutput);
|
|
344
|
+
stage = errorOutput.stage || detectStage(errorMsg);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Match against known patterns
|
|
348
|
+
for (const { pattern, category, retryable } of ERROR_PATTERNS) {
|
|
349
|
+
if (pattern.test(errorMsg)) {
|
|
350
|
+
return { stage, category, retryable };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Default: unrecognized errors are classified as code_bug (non-retryable)
|
|
355
|
+
return { stage, category: 'code_bug', retryable: false };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Detect which lifecycle stage produced an error from the error message.
|
|
360
|
+
*
|
|
361
|
+
* @param {string} errorMsg - The error message
|
|
362
|
+
* @returns {string} The detected stage name
|
|
363
|
+
*/
|
|
364
|
+
function detectStage(errorMsg) {
|
|
365
|
+
const stagePatterns = [
|
|
366
|
+
{ pattern: /\b(generate|generation)\b/i, stage: 'generate' },
|
|
367
|
+
{ pattern: /\b(build|docker)\b/i, stage: 'build' },
|
|
368
|
+
{ pattern: /\b(push|ecr|registry)\b/i, stage: 'push' },
|
|
369
|
+
{ pattern: /\b(deploy|endpoint|CreateEndpoint|InferenceComponent)\b/i, stage: 'deploy' },
|
|
370
|
+
{ pattern: /\b(test|invoke|invocation|inference)\b/i, stage: 'test' },
|
|
371
|
+
{ pattern: /\b(tune|fine-?tun|customization)\b/i, stage: 'tune' },
|
|
372
|
+
{ pattern: /\b(adapter|lora)\b/i, stage: 'adapter' },
|
|
373
|
+
{ pattern: /\b(benchmark|bench)\b/i, stage: 'benchmark' },
|
|
374
|
+
{ pattern: /\b(register|dynamo)\b/i, stage: 'register' },
|
|
375
|
+
{ pattern: /\b(clean|delete)\b/i, stage: 'clean' }
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
for (const { pattern, stage } of stagePatterns) {
|
|
379
|
+
if (pattern.test(errorMsg)) {
|
|
380
|
+
return stage;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return 'unknown';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Result Writing (Task 5.5) ────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Build a Path Prover Athena record from execution result and classification.
|
|
391
|
+
*
|
|
392
|
+
* All records have run_type='path_prove'. On success, status='completed'.
|
|
393
|
+
* On non-retryable failure, status='unfeasible' with failure_reason populated.
|
|
394
|
+
* On retryable failure, status='failed' with failure_reason populated.
|
|
395
|
+
*
|
|
396
|
+
* @param {object} result - The execution result
|
|
397
|
+
* @param {boolean} result.success - Whether the prove run succeeded
|
|
398
|
+
* @param {object} [result.metrics] - Benchmark metrics (on success)
|
|
399
|
+
* @param {object} [result.config] - The config that was proven
|
|
400
|
+
* @param {string} [result.error] - Error message (on failure)
|
|
401
|
+
* @param {object|null} [classification] - Failure classification (from classifyFailure)
|
|
402
|
+
* @param {string} [classification.stage] - Stage that failed
|
|
403
|
+
* @param {string} [classification.category] - Error category
|
|
404
|
+
* @param {boolean} [classification.retryable] - Whether failure is retryable
|
|
405
|
+
* @returns {object} Athena-compatible record with run_type='path_prove'
|
|
406
|
+
*/
|
|
407
|
+
export function buildPathProverRecord(result, classification) {
|
|
408
|
+
const record = {
|
|
409
|
+
run_type: 'path_prove',
|
|
410
|
+
run_timestamp: new Date().toISOString()
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Merge config dimensions if provided
|
|
414
|
+
if (result.config) {
|
|
415
|
+
for (const dim of CONFIG_DIMENSIONS) {
|
|
416
|
+
if (result.config[dim] !== undefined) {
|
|
417
|
+
record[dim] = result.config[dim];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Also copy non-dimension config fields
|
|
421
|
+
if (result.config.config_id) record.config_id = result.config.config_id;
|
|
422
|
+
if (result.config.model_name) record.model_name = result.config.model_name;
|
|
423
|
+
if (result.config.instance_type) record.instance_type = result.config.instance_type;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (result.success) {
|
|
427
|
+
record.status = 'completed';
|
|
428
|
+
// Merge metrics if available
|
|
429
|
+
if (result.metrics) {
|
|
430
|
+
Object.assign(record, result.metrics);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
// Failure case
|
|
434
|
+
if (classification && classification.retryable === false) {
|
|
435
|
+
record.status = 'unfeasible';
|
|
436
|
+
} else {
|
|
437
|
+
record.status = 'failed';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Populate failure details
|
|
441
|
+
record.failure_reason = result.error || 'Unknown failure';
|
|
442
|
+
|
|
443
|
+
if (classification) {
|
|
444
|
+
record.failure_stage = classification.stage;
|
|
445
|
+
record.failure_category = classification.category;
|
|
446
|
+
record.failure_retryable = classification.retryable;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return record;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Check if a config is known to be unfeasible (prevents repeated attempts).
|
|
455
|
+
*
|
|
456
|
+
* @param {object} config - The config to check
|
|
457
|
+
* @param {object[]} existingRecords - Existing Athena records
|
|
458
|
+
* @returns {object|null} The unfeasible record if found, null otherwise
|
|
459
|
+
*/
|
|
460
|
+
export function findUnfeasibleRecord(config, existingRecords) {
|
|
461
|
+
if (!config || !existingRecords || existingRecords.length === 0) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
for (const record of existingRecords) {
|
|
466
|
+
if (record.status !== 'unfeasible') continue;
|
|
467
|
+
if (record.run_type !== 'path_prove') continue;
|
|
468
|
+
|
|
469
|
+
// Check if all dimensions match
|
|
470
|
+
const allMatch = CONFIG_DIMENSIONS.every(dim =>
|
|
471
|
+
String(record[dim] ?? '') === String(config[dim] ?? '')
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (allMatch) return record;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── Priority Queue (v1 Validation Mode) ──────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Get the next unproven config from the priority queue.
|
|
484
|
+
*
|
|
485
|
+
* Checks the priority targets list and returns the first target whose
|
|
486
|
+
* status is 'pending' and which hasn't been proven in existing records.
|
|
487
|
+
* If all priority targets are proven/completed, returns null to fall
|
|
488
|
+
* through to gap-finding mode.
|
|
489
|
+
*
|
|
490
|
+
* @param {object} event - The Step Functions event object
|
|
491
|
+
* @param {string} [event.priorityConfigPath] - Path to priority targets JSON
|
|
492
|
+
* @param {object[]} [event.previousResults] - Previously proven configs in this run
|
|
493
|
+
* @param {object|null} priorityData - Pre-loaded priority data (for Lambda/testing).
|
|
494
|
+
* If null, attempts to load from event.priorityConfigPath.
|
|
495
|
+
* @returns {object|null} Next config to prove, or null if priority queue exhausted
|
|
496
|
+
*/
|
|
497
|
+
export function getNextPriorityConfig(event, priorityData = null) {
|
|
498
|
+
// Resolve priority data: explicit param > event._priorityData > load from file
|
|
499
|
+
const data = priorityData || event._priorityData || (
|
|
500
|
+
event.priorityConfigPath ? loadPriorityTargets(event.priorityConfigPath) : null
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (!data || !data.targets || !Array.isArray(data.targets)) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const defaults = data.defaults || {};
|
|
508
|
+
const provenNames = new Set((data.proven || []).map(p => p.model_name));
|
|
509
|
+
|
|
510
|
+
// Also consider previousResults from this run as proven
|
|
511
|
+
const previousResults = event.previousResults || [];
|
|
512
|
+
for (const result of previousResults) {
|
|
513
|
+
if (result.success && result.config && result.config.model_name) {
|
|
514
|
+
provenNames.add(result.config.model_name);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Find first pending target not yet proven
|
|
519
|
+
for (const target of data.targets) {
|
|
520
|
+
if (target.status !== 'pending') continue;
|
|
521
|
+
if (provenNames.has(target.model_name)) continue;
|
|
522
|
+
|
|
523
|
+
// Build full config from defaults + target overrides
|
|
524
|
+
const config = { ...defaults, ...target };
|
|
525
|
+
delete config.status; // status is metadata, not a config field
|
|
526
|
+
|
|
527
|
+
return config;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// All priority targets are proven or non-pending
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Update a priority target's status after a prove attempt.
|
|
536
|
+
*
|
|
537
|
+
* @param {object} priorityData - The loaded priority targets data (mutated in place)
|
|
538
|
+
* @param {string} modelName - The model_name to update
|
|
539
|
+
* @param {string} newStatus - New status: 'proven', 'failed', or 'unfeasible'
|
|
540
|
+
* @param {object} [details] - Additional details (error_category, error_message)
|
|
541
|
+
* @returns {object} Updated priority data (same reference, mutated)
|
|
542
|
+
*/
|
|
543
|
+
export function updatePriorityStatus(priorityData, modelName, newStatus, details = {}) {
|
|
544
|
+
if (!priorityData || !priorityData.targets) return priorityData;
|
|
545
|
+
|
|
546
|
+
const targetIndex = priorityData.targets.findIndex(t => t.model_name === modelName);
|
|
547
|
+
if (targetIndex === -1) return priorityData;
|
|
548
|
+
|
|
549
|
+
if (newStatus === 'proven') {
|
|
550
|
+
// Move from targets to proven list
|
|
551
|
+
priorityData.targets.splice(targetIndex, 1);
|
|
552
|
+
priorityData.proven = priorityData.proven || [];
|
|
553
|
+
priorityData.proven.push({
|
|
554
|
+
model_name: modelName,
|
|
555
|
+
proven_date: new Date().toISOString().split('T')[0],
|
|
556
|
+
...details
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
// Update status in place (failed, unfeasible)
|
|
560
|
+
const target = priorityData.targets[targetIndex];
|
|
561
|
+
target.status = newStatus;
|
|
562
|
+
if (details.error_category) target.error_category = details.error_category;
|
|
563
|
+
if (details.error_message) target.error_message = details.error_message;
|
|
564
|
+
target.last_attempt = new Date().toISOString();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return priorityData;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get a summary of priority queue status.
|
|
572
|
+
*
|
|
573
|
+
* @param {object} priorityData - The loaded priority targets data
|
|
574
|
+
* @returns {object} Summary with counts: { total, pending, proven, failed, unfeasible }
|
|
575
|
+
*/
|
|
576
|
+
export function getPriorityQueueStatus(priorityData) {
|
|
577
|
+
if (!priorityData) {
|
|
578
|
+
return { total: 0, pending: 0, proven: 0, failed: 0, unfeasible: 0 };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const targets = priorityData.targets || [];
|
|
582
|
+
const proven = priorityData.proven || [];
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
total: targets.length + proven.length,
|
|
586
|
+
pending: targets.filter(t => t.status === 'pending').length,
|
|
587
|
+
proven: proven.length,
|
|
588
|
+
failed: targets.filter(t => t.status === 'failed').length,
|
|
589
|
+
unfeasible: targets.filter(t => t.status === 'unfeasible').length
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Load priority targets from a JSON file path (synchronous).
|
|
595
|
+
*
|
|
596
|
+
* @param {string} configPath - Absolute or relative path to the JSON file
|
|
597
|
+
* @returns {object|null} Parsed priority data, or null if not found/invalid
|
|
598
|
+
*/
|
|
599
|
+
export function loadPriorityTargets(configPath) {
|
|
600
|
+
try {
|
|
601
|
+
const resolvedPath = resolve(configPath);
|
|
602
|
+
const raw = readFileSync(resolvedPath, 'utf8');
|
|
603
|
+
return JSON.parse(raw);
|
|
604
|
+
} catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|