@aws/ml-container-creator 0.13.5 → 0.15.1
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/config/parameter-schema-v2.json +33 -5
- package/infra/ci-harness/lib/ci-harness-stack.ts +13 -5
- package/infra/ci-harness/package-lock.json +121 -111
- package/infra/ci-harness/package.json +1 -1
- package/package.json +2 -2
- package/servers/endpoint-picker/index.js +23 -14
- package/servers/instance-sizer/index.js +72 -4
- package/servers/instance-sizer/lib/model-resolver.js +28 -2
- package/src/app.js +15 -0
- package/src/lib/config-loader.js +18 -0
- package/src/lib/config-manager.js +6 -1
- package/src/lib/dataset-slug.js +152 -0
- package/src/lib/generated/cli-options.js +9 -3
- package/src/lib/generated/parameter-matrix.js +15 -4
- package/src/lib/generated/validation-rules.js +1 -1
- package/src/lib/mcp-client.js +15 -1
- package/src/lib/mcp-query-runner.js +11 -1
- package/src/lib/prompt-runner.js +40 -20
- package/src/lib/prompts/feature-prompts.js +1 -1
- package/src/lib/template-manager.js +0 -7
- package/src/lib/template-variable-resolver.js +51 -1
- package/src/lib/tune-config-state.js +14 -1
- package/templates/do/.benchmark_writer.py +43 -0
- package/templates/do/.register_helper.py +1185 -0
- package/templates/do/.tune_helper.py +168 -2
- package/templates/do/__pycache__/.adapter_helper.cpython-312.pyc +0 -0
- package/templates/do/__pycache__/.benchmark_writer.cpython-312.pyc +0 -0
- package/templates/do/__pycache__/.register_helper.cpython-312.pyc +0 -0
- package/templates/do/__pycache__/.tune_helper.cpython-312.pyc +0 -0
- package/templates/do/adapter +319 -27
- package/templates/do/add-ic +85 -3
- package/templates/do/benchmark +28 -8
- package/templates/do/config +20 -0
- package/templates/do/lib/inference-component.sh +56 -3
- package/templates/do/register +557 -6
- package/templates/do/test +12 -2
- package/templates/do/tune +219 -6
package/templates/do/tune
CHANGED
|
@@ -46,10 +46,13 @@ ARG_DRY_RUN=false
|
|
|
46
46
|
ARG_LIST_MODELS=false
|
|
47
47
|
ARG_NO_STALE_WARNING=false
|
|
48
48
|
ARG_DISCOVER=false
|
|
49
|
+
ARG_LIST_DATASETS=false
|
|
49
50
|
ARG_DISCOVER_FILTER=""
|
|
50
51
|
ARG_COLUMN_MAP=""
|
|
51
52
|
ARG_TAKE=""
|
|
52
53
|
ARG_ACCEPT_EULA=false
|
|
54
|
+
ARG_DATASET_NAME=""
|
|
55
|
+
ARG_EVALUATOR_NAME=""
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
# ── _parse_args() ─────────────────────────────────────────────────────────────
|
|
@@ -148,6 +151,7 @@ _parse_args() {
|
|
|
148
151
|
--help|-h) ARG_HELP=true; shift ;;
|
|
149
152
|
--dry-run) ARG_DRY_RUN=true; shift ;;
|
|
150
153
|
--list-models) ARG_LIST_MODELS=true; shift ;;
|
|
154
|
+
--list-datasets) ARG_LIST_DATASETS=true; shift ;;
|
|
151
155
|
--no-stale-warning) ARG_NO_STALE_WARNING=true; shift ;;
|
|
152
156
|
--column-map)
|
|
153
157
|
if [ -z "${2:-}" ]; then
|
|
@@ -169,6 +173,18 @@ _parse_args() {
|
|
|
169
173
|
exit 1
|
|
170
174
|
fi
|
|
171
175
|
ARG_TAKE="$2"; shift 2 ;;
|
|
176
|
+
--dataset-name)
|
|
177
|
+
if [ -z "${2:-}" ]; then
|
|
178
|
+
echo "❌ --dataset-name requires a registered dataset name"
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
181
|
+
ARG_DATASET_NAME="$2"; shift 2 ;;
|
|
182
|
+
--evaluator-name)
|
|
183
|
+
if [ -z "${2:-}" ]; then
|
|
184
|
+
echo "❌ --evaluator-name requires a registered evaluator name"
|
|
185
|
+
exit 1
|
|
186
|
+
fi
|
|
187
|
+
ARG_EVALUATOR_NAME="$2"; shift 2 ;;
|
|
172
188
|
*)
|
|
173
189
|
echo "❌ Unknown option: $1"
|
|
174
190
|
echo " Run ./do/tune --help for usage."
|
|
@@ -775,10 +791,69 @@ else:
|
|
|
775
791
|
_validate_dataset() {
|
|
776
792
|
local dataset="${ARG_DATASET}"
|
|
777
793
|
|
|
794
|
+
# If --dataset-name is set, resolve from registry (AC-2b.4)
|
|
795
|
+
# --dataset-name takes precedence over --dataset for named registry lookup
|
|
796
|
+
if [ -n "${ARG_DATASET_NAME}" ]; then
|
|
797
|
+
echo "🔍 Resolving dataset '${ARG_DATASET_NAME}' from registry..."
|
|
798
|
+
local resolve_result
|
|
799
|
+
resolve_result=$(python3 "${SCRIPT_DIR}/.register_helper.py" resolve-dataset \
|
|
800
|
+
--name "${ARG_DATASET_NAME}" 2>/dev/null) || resolve_result=""
|
|
801
|
+
|
|
802
|
+
if [ -n "${resolve_result}" ]; then
|
|
803
|
+
local resolved_uri
|
|
804
|
+
resolved_uri=$(echo "${resolve_result}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('s3_uri',''))" 2>/dev/null) || resolved_uri=""
|
|
805
|
+
if [ -n "${resolved_uri}" ]; then
|
|
806
|
+
echo " Resolved to: ${resolved_uri}"
|
|
807
|
+
dataset="${resolved_uri}"
|
|
808
|
+
ARG_DATASET="${resolved_uri}"
|
|
809
|
+
else
|
|
810
|
+
echo "❌ Dataset '${ARG_DATASET_NAME}' not found in registry"
|
|
811
|
+
echo " Register it first: ./do/register --dataset --dataset-name ${ARG_DATASET_NAME} --dataset-s3-uri s3://..."
|
|
812
|
+
exit 1
|
|
813
|
+
fi
|
|
814
|
+
else
|
|
815
|
+
echo "❌ Failed to resolve dataset '${ARG_DATASET_NAME}' from registry"
|
|
816
|
+
echo " Register it first: ./do/register --dataset --dataset-name ${ARG_DATASET_NAME} --dataset-s3-uri s3://..."
|
|
817
|
+
exit 1
|
|
818
|
+
fi
|
|
819
|
+
fi
|
|
820
|
+
|
|
821
|
+
# If --dataset value is not an S3 URI or HF reference, treat as a registry name
|
|
822
|
+
if [ -z "${ARG_DATASET_NAME}" ] && [ -n "${dataset}" ] && \
|
|
823
|
+
[[ "${dataset}" != s3://* ]] && [[ "${dataset}" != hf://* ]]; then
|
|
824
|
+
# Looks like a name — resolve from registry
|
|
825
|
+
ARG_DATASET_NAME="${dataset}"
|
|
826
|
+
dataset=""
|
|
827
|
+
fi
|
|
828
|
+
|
|
778
829
|
if [ -z "${dataset}" ]; then
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
830
|
+
if [ -n "${ARG_DATASET_NAME}" ]; then
|
|
831
|
+
# Name-based resolution happens below via resolve-dataset
|
|
832
|
+
echo "🔍 Resolving dataset '${ARG_DATASET_NAME}' from registry..."
|
|
833
|
+
local resolve_result
|
|
834
|
+
resolve_result=$(python3 "${SCRIPT_DIR}/.register_helper.py" resolve-dataset \
|
|
835
|
+
--name "${ARG_DATASET_NAME}" 2>/dev/null) || resolve_result=""
|
|
836
|
+
|
|
837
|
+
if [ -n "${resolve_result}" ]; then
|
|
838
|
+
local resolved_uri
|
|
839
|
+
resolved_uri=$(echo "${resolve_result}" | grep -E '^\{' | tail -1 | python3 -c "import sys,json; print(json.load(sys.stdin).get('s3_uri',''))" 2>/dev/null) || resolved_uri=""
|
|
840
|
+
if [ -n "${resolved_uri}" ]; then
|
|
841
|
+
echo " Resolved to: ${resolved_uri}"
|
|
842
|
+
dataset="${resolved_uri}"
|
|
843
|
+
RESOLVED_DATASET_S3_URI="${resolved_uri}"
|
|
844
|
+
return 0
|
|
845
|
+
fi
|
|
846
|
+
fi
|
|
847
|
+
echo "❌ Dataset '${ARG_DATASET_NAME}' not found in registry"
|
|
848
|
+
echo " Run ./do/tune --list-datasets to see available datasets."
|
|
849
|
+
echo " Register: ./do/register dataset <name> --s3-uri <uri> --technique <sft|dpo>"
|
|
850
|
+
exit 1
|
|
851
|
+
else
|
|
852
|
+
echo "❌ --dataset is required"
|
|
853
|
+
echo " Provide an S3 URI (s3://bucket/path.jsonl), HF reference (hf://org/name), or registered name"
|
|
854
|
+
echo " Run ./do/tune --list-datasets to see available registered datasets."
|
|
855
|
+
exit 1
|
|
856
|
+
fi
|
|
782
857
|
fi
|
|
783
858
|
|
|
784
859
|
# Determine dataset type
|
|
@@ -908,11 +983,10 @@ _validate_dataset() {
|
|
|
908
983
|
fi
|
|
909
984
|
|
|
910
985
|
RESOLVED_DATASET_S3_URI=$(echo "${stage_result}" | python3 -c "import sys,json; print(json.load(sys.stdin)['s3_uri'])" 2>/dev/null)
|
|
911
|
-
|
|
912
|
-
num_records=$(echo "${stage_result}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('num_records',0))" 2>/dev/null) || num_records="0"
|
|
986
|
+
RESOLVED_DATASET_ROW_COUNT=$(echo "${stage_result}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('num_records',0))" 2>/dev/null) || RESOLVED_DATASET_ROW_COUNT="0"
|
|
913
987
|
|
|
914
988
|
echo " ✅ Staged to: ${RESOLVED_DATASET_S3_URI}"
|
|
915
|
-
echo " Records: ${
|
|
989
|
+
echo " Records: ${RESOLVED_DATASET_ROW_COUNT}"
|
|
916
990
|
echo ""
|
|
917
991
|
|
|
918
992
|
else
|
|
@@ -1126,6 +1200,37 @@ print(entry.get('provider', ''))
|
|
|
1126
1200
|
if [ -n "${ARG_REWARD_PROMPT}" ]; then
|
|
1127
1201
|
submit_args+=(--reward-prompt "${ARG_REWARD_PROMPT}")
|
|
1128
1202
|
fi
|
|
1203
|
+
|
|
1204
|
+
# Resolve evaluator from registry if --evaluator-name is set (AC-2c.3, AC-2c.4)
|
|
1205
|
+
if [ -n "${ARG_EVALUATOR_NAME}" ] && [ -z "${ARG_REWARD_FUNCTION}" ] && [ -z "${ARG_REWARD_PROMPT}" ]; then
|
|
1206
|
+
echo "🔍 Resolving evaluator '${ARG_EVALUATOR_NAME}' from registry..."
|
|
1207
|
+
local ev_resolve_result
|
|
1208
|
+
ev_resolve_result=$(python3 "${SCRIPT_DIR}/.register_helper.py" resolve-evaluator \
|
|
1209
|
+
--name "${ARG_EVALUATOR_NAME}" 2>/dev/null) || ev_resolve_result=""
|
|
1210
|
+
|
|
1211
|
+
if [ -n "${ev_resolve_result}" ]; then
|
|
1212
|
+
local ev_type ev_arn_or_uri
|
|
1213
|
+
ev_type=$(echo "${ev_resolve_result}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('type',''))" 2>/dev/null) || ev_type=""
|
|
1214
|
+
ev_arn_or_uri=$(echo "${ev_resolve_result}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('arn_or_uri',''))" 2>/dev/null) || ev_arn_or_uri=""
|
|
1215
|
+
|
|
1216
|
+
if [ -n "${ev_arn_or_uri}" ]; then
|
|
1217
|
+
echo " Resolved evaluator: ${ev_type} → ${ev_arn_or_uri}"
|
|
1218
|
+
if [ "${ev_type}" = "lambda" ]; then
|
|
1219
|
+
submit_args+=(--reward-function "${ev_arn_or_uri}")
|
|
1220
|
+
else
|
|
1221
|
+
submit_args+=(--reward-prompt "${ev_arn_or_uri}")
|
|
1222
|
+
fi
|
|
1223
|
+
else
|
|
1224
|
+
echo "⚠️ Evaluator '${ARG_EVALUATOR_NAME}' not found in registry"
|
|
1225
|
+
echo " Register it first: ./do/register --evaluator --evaluator-name ${ARG_EVALUATOR_NAME} ..."
|
|
1226
|
+
exit 1
|
|
1227
|
+
fi
|
|
1228
|
+
else
|
|
1229
|
+
echo "⚠️ Failed to resolve evaluator '${ARG_EVALUATOR_NAME}' from registry"
|
|
1230
|
+
echo " Register it first: ./do/register --evaluator --evaluator-name ${ARG_EVALUATOR_NAME} ..."
|
|
1231
|
+
exit 1
|
|
1232
|
+
fi
|
|
1233
|
+
fi
|
|
1129
1234
|
if [ "${ARG_ACCEPT_EULA}" = true ]; then
|
|
1130
1235
|
submit_args+=(--accept-eula)
|
|
1131
1236
|
fi
|
|
@@ -1217,6 +1322,14 @@ print(entry.get('provider', ''))
|
|
|
1217
1322
|
_update_config_var "TUNE_TECHNIQUE" "${ARG_TECHNIQUE}"
|
|
1218
1323
|
_update_config_var "TUNE_TRAINING_TYPE" "${ARG_TRAINING_TYPE}"
|
|
1219
1324
|
_update_config_var "TUNE_DATASET_PATH" "${ARG_DATASET}"
|
|
1325
|
+
_update_config_var "TUNE_DATASET_S3_URI" "${RESOLVED_DATASET_S3_URI:-}"
|
|
1326
|
+
_update_config_var "TUNE_DATASET_ROW_COUNT" "${RESOLVED_DATASET_ROW_COUNT:-0}"
|
|
1327
|
+
_update_config_var "TUNE_DATASET_SOURCE" "${ARG_DATASET}"
|
|
1328
|
+
_update_config_var "TUNE_LAST_JOB_NAME" "${JOB_NAME}"
|
|
1329
|
+
# Technique-specific dataset vars (allows --from-tune <technique> resolution)
|
|
1330
|
+
_update_config_var "TUNE_DATASET_S3_URI_${technique_upper}" "${RESOLVED_DATASET_S3_URI:-}"
|
|
1331
|
+
_update_config_var "TUNE_DATASET_ROW_COUNT_${technique_upper}" "${RESOLVED_DATASET_ROW_COUNT:-0}"
|
|
1332
|
+
_update_config_var "TUNE_DATASET_SOURCE_${technique_upper}" "${ARG_DATASET}"
|
|
1220
1333
|
}
|
|
1221
1334
|
|
|
1222
1335
|
|
|
@@ -1292,6 +1405,56 @@ _handle_interrupt() {
|
|
|
1292
1405
|
exit 130
|
|
1293
1406
|
}
|
|
1294
1407
|
|
|
1408
|
+
# ── _derive_dataset_slug() ────────────────────────────────────────────────────
|
|
1409
|
+
# Derive a short slug from the dataset argument for config variable naming.
|
|
1410
|
+
# Rules: lowercase, strip non-alphanumeric (keep hyphens), truncate to 20 chars,
|
|
1411
|
+
# collapse consecutive hyphens, strip leading/trailing hyphens.
|
|
1412
|
+
#
|
|
1413
|
+
# Examples:
|
|
1414
|
+
# hf://tatsu-lab/alpaca → alpaca
|
|
1415
|
+
# hf://Open-Orca/OpenOrca → openorca
|
|
1416
|
+
# s3://bucket/path/train.jsonl → train
|
|
1417
|
+
# s3://bucket/path/file.parquet → file
|
|
1418
|
+
_derive_dataset_slug() {
|
|
1419
|
+
local dataset="${1:-}"
|
|
1420
|
+
local slug=""
|
|
1421
|
+
|
|
1422
|
+
if [ -z "${dataset}" ]; then
|
|
1423
|
+
echo ""
|
|
1424
|
+
return
|
|
1425
|
+
fi
|
|
1426
|
+
|
|
1427
|
+
if [[ "${dataset}" == hf://* ]]; then
|
|
1428
|
+
# HuggingFace format: hf://org/name or hf://org/name?file=...
|
|
1429
|
+
# Strip query params
|
|
1430
|
+
local hf_path="${dataset#hf://}"
|
|
1431
|
+
hf_path="${hf_path%%\?*}"
|
|
1432
|
+
# Take the last component (dataset name, not org)
|
|
1433
|
+
slug="${hf_path##*/}"
|
|
1434
|
+
elif [[ "${dataset}" == s3://* ]]; then
|
|
1435
|
+
# S3 format: s3://bucket/path/file.ext → slug from filename without extension
|
|
1436
|
+
local filename="${dataset##*/}"
|
|
1437
|
+
slug="${filename%.*}"
|
|
1438
|
+
else
|
|
1439
|
+
# Fallback: use the last path component without extension
|
|
1440
|
+
local filename="${dataset##*/}"
|
|
1441
|
+
slug="${filename%.*}"
|
|
1442
|
+
fi
|
|
1443
|
+
|
|
1444
|
+
# Slugification: lowercase, strip non-alphanumeric (keep hyphens)
|
|
1445
|
+
slug=$(echo "${slug}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]//g')
|
|
1446
|
+
# Collapse consecutive hyphens
|
|
1447
|
+
slug=$(echo "${slug}" | sed 's/-\{2,\}/-/g')
|
|
1448
|
+
# Strip leading/trailing hyphens
|
|
1449
|
+
slug=$(echo "${slug}" | sed 's/^-//;s/-$//')
|
|
1450
|
+
# Truncate to 20 chars
|
|
1451
|
+
slug="${slug:0:20}"
|
|
1452
|
+
# Strip trailing hyphen after truncation
|
|
1453
|
+
slug=$(echo "${slug}" | sed 's/-$//')
|
|
1454
|
+
|
|
1455
|
+
echo "${slug}"
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1295
1458
|
# ── _handle_completion() ──────────────────────────────────────────────────────
|
|
1296
1459
|
# Store output paths, detect output type, print next-step commands.
|
|
1297
1460
|
_handle_completion() {
|
|
@@ -1351,6 +1514,14 @@ _handle_completion() {
|
|
|
1351
1514
|
# Store output paths in config
|
|
1352
1515
|
if [ "${output_type}" = "adapter" ]; then
|
|
1353
1516
|
_update_config_var "TUNE_ADAPTER_PATH_${technique_upper}" "${artifact_path}"
|
|
1517
|
+
# Write dataset-specific adapter path if dataset slug is available
|
|
1518
|
+
local dataset_slug
|
|
1519
|
+
dataset_slug=$(_derive_dataset_slug "${ARG_DATASET:-}")
|
|
1520
|
+
if [ -n "${dataset_slug}" ]; then
|
|
1521
|
+
local slug_upper
|
|
1522
|
+
slug_upper=$(echo "${dataset_slug}" | tr '[:lower:]' '[:upper:]' | sed 's/-/_/g')
|
|
1523
|
+
_update_config_var "TUNE_ADAPTER_PATH_${technique_upper}_${slug_upper}" "${artifact_path}"
|
|
1524
|
+
fi
|
|
1354
1525
|
else
|
|
1355
1526
|
_update_config_var "TUNE_MODEL_PATH_${technique_upper}" "${artifact_path}"
|
|
1356
1527
|
fi
|
|
@@ -1361,9 +1532,14 @@ _handle_completion() {
|
|
|
1361
1532
|
echo "📋 Next steps:"
|
|
1362
1533
|
echo ""
|
|
1363
1534
|
if [ "${output_type}" = "adapter" ]; then
|
|
1535
|
+
local dataset_slug
|
|
1536
|
+
dataset_slug=$(_derive_dataset_slug "${ARG_DATASET:-}")
|
|
1364
1537
|
echo " Deploy as LoRA adapter:"
|
|
1365
1538
|
echo " ./do/adapter add tuned-${ARG_TECHNIQUE} --from-tune"
|
|
1366
1539
|
echo " ./do/adapter add tuned-${ARG_TECHNIQUE} --from-tune ${ARG_TECHNIQUE}"
|
|
1540
|
+
if [ -n "${dataset_slug}" ]; then
|
|
1541
|
+
echo " ./do/adapter add tuned-${ARG_TECHNIQUE}-${dataset_slug} --from-tune ${ARG_TECHNIQUE}-${dataset_slug}"
|
|
1542
|
+
fi
|
|
1367
1543
|
echo " ./do/adapter add tuned-${ARG_TECHNIQUE} --weights ${artifact_path}"
|
|
1368
1544
|
else
|
|
1369
1545
|
echo " Deploy as new inference component:"
|
|
@@ -1460,6 +1636,43 @@ if [ "${ARG_DISCOVER}" = true ]; then
|
|
|
1460
1636
|
exit 0
|
|
1461
1637
|
fi
|
|
1462
1638
|
|
|
1639
|
+
# Handle --list-datasets (before requiring --technique and --dataset)
|
|
1640
|
+
if [ "${ARG_LIST_DATASETS}" = true ]; then
|
|
1641
|
+
echo ""
|
|
1642
|
+
echo "📦 Registered datasets:"
|
|
1643
|
+
echo ""
|
|
1644
|
+
|
|
1645
|
+
_ds_output=$(python3 "${SCRIPT_DIR}/.register_helper.py" list-datasets 2>/dev/null) || _ds_output=""
|
|
1646
|
+
_ds_json=$(echo "${_ds_output}" | grep -E '^\{' | tail -1)
|
|
1647
|
+
|
|
1648
|
+
if [ -n "${_ds_json}" ]; then
|
|
1649
|
+
_ds_count=$(echo "${_ds_json}" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('datasets',[])))" 2>/dev/null) || _ds_count=0
|
|
1650
|
+
if [ "${_ds_count}" -gt 0 ]; then
|
|
1651
|
+
printf " %-25s %-10s %-8s %s\n" "NAME" "TECHNIQUE" "ROWS" "S3 URI"
|
|
1652
|
+
printf " %-25s %-10s %-8s %s\n" "----" "---------" "----" "------"
|
|
1653
|
+
echo "${_ds_json}" | python3 -c "
|
|
1654
|
+
import sys, json
|
|
1655
|
+
data = json.load(sys.stdin)
|
|
1656
|
+
for ds in data.get('datasets', []):
|
|
1657
|
+
name = ds.get('name','')[:25]
|
|
1658
|
+
tech = ds.get('technique','')[:10]
|
|
1659
|
+
rows = str(ds.get('row_count',''))[:8]
|
|
1660
|
+
uri = ds.get('s3_uri','')
|
|
1661
|
+
print(f' {name:<25} {tech:<10} {rows:<8} {uri}')
|
|
1662
|
+
" 2>/dev/null
|
|
1663
|
+
else
|
|
1664
|
+
echo " (none registered)"
|
|
1665
|
+
fi
|
|
1666
|
+
else
|
|
1667
|
+
echo " (none registered)"
|
|
1668
|
+
fi
|
|
1669
|
+
echo ""
|
|
1670
|
+
echo " Register: ./do/register dataset <name> --s3-uri <uri> --technique <sft|dpo>"
|
|
1671
|
+
echo " Use: ./do/tune --technique sft --dataset <name>"
|
|
1672
|
+
echo ""
|
|
1673
|
+
exit 0
|
|
1674
|
+
fi
|
|
1675
|
+
|
|
1463
1676
|
# Validate required arguments for job submission
|
|
1464
1677
|
if [ -z "${ARG_TECHNIQUE}" ]; then
|
|
1465
1678
|
echo "❌ --technique is required"
|