Files
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

662 lines
24 KiB
Python

# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
import json
import os
import os.path
import uuid
import pytest
from ludwig.backend import initialize_backend
from ludwig.constants import (
ACCURACY,
AUTO,
BATCH_SIZE,
CATEGORY,
COMBINER,
EXECUTOR,
HYPEROPT,
INPUT_FEATURES,
MAX_CONCURRENT_TRIALS,
MODEL_ECD,
MODEL_TYPE,
NAME,
OUTPUT_FEATURES,
RAY,
TEXT,
TRAINER,
TYPE,
VALIDATION,
)
from ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME, MODEL_FILE_NAME
from ludwig.hyperopt.results import HyperoptResults
from ludwig.hyperopt.run import hyperopt
from ludwig.hyperopt.utils import update_hyperopt_params_with_defaults
from ludwig.schema.model_config import ModelConfig
from ludwig.utils import fs_utils
from ludwig.utils.data_utils import load_json, use_credentials
from tests.integration_tests.utils import category_feature, generate_data, minio_test_creds, remote_tmpdir, text_feature
ray = pytest.importorskip("ray")
from ludwig.hyperopt.execution import get_build_hyperopt_executor, RayTuneExecutor # noqa
pytestmark = [pytest.mark.distributed, pytest.mark.distributed_c, pytest.mark.integration_tests_c]
RANDOM_SEARCH_SIZE = 2
HYPEROPT_CONFIG = {
"parameters": {
# using only float parameter as common in all search algorithms
"trainer.learning_rate": {"space": "loguniform", "lower": 0.001, "upper": 0.1},
},
"goal": "minimize",
"executor": {TYPE: "ray", "num_samples": 2, "scheduler": {TYPE: "fifo"}},
"search_alg": {TYPE: "variant_generator"},
}
SEARCH_ALGS_FOR_TESTING = [
# None,
# "variant_generator",
"random",
"bohb",
# "hyperopt",
# "ax",
# "bayesopt",
# "blendsearch",
# "cfo",
# "dragonfly",
# "hebo",
# "skopt",
# "optuna",
]
SCHEDULERS_FOR_TESTING = [
"fifo",
"asynchyperband",
# "async_hyperband",
# "median_stopping_rule",
# "medianstopping",
# "hyperband",
# "hb_bohb",
# "pbt",
# "pb2", commented out for now: https://github.com/ray-project/ray/issues/24815
# "resource_changing",
]
def _setup_ludwig_config(dataset_fp: str, model_type: str = MODEL_ECD) -> tuple[dict, str]:
input_features = [category_feature(encoder={"vocab_size": 3})]
output_features = [category_feature(decoder={"vocab_size": 3})]
rel_path = generate_data(input_features, output_features, dataset_fp, num_examples=30)
trainer_cfg = {"learning_rate": 0.001}
if model_type == MODEL_ECD:
trainer_cfg["epochs"] = 2
else:
trainer_cfg["num_boost_round"] = 2
# Disable feature filtering to avoid having no features due to small test dataset,
# see https://stackoverflow.com/a/66405983/5222402
trainer_cfg["feature_pre_filter"] = False
config = {
MODEL_TYPE: model_type,
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
COMBINER: {TYPE: "concat"},
TRAINER: trainer_cfg,
}
config = ModelConfig.from_dict(config).to_dict()
return config, rel_path
@pytest.mark.parametrize("search_alg", SEARCH_ALGS_FOR_TESTING)
@pytest.mark.parametrize("model_type", [MODEL_ECD])
def test_hyperopt_search_alg(
search_alg,
model_type,
csv_filename,
tmpdir,
ray_cluster_7cpu,
validate_output_feature=False,
validation_metric=None,
split="validation",
):
config, rel_path = _setup_ludwig_config(csv_filename, model_type)
hyperopt_config = HYPEROPT_CONFIG.copy()
# finalize hyperopt config settings
if search_alg == "dragonfly":
hyperopt_config["search_alg"] = {
TYPE: search_alg,
"domain": "euclidean",
"optimizer": "random",
}
elif search_alg is None:
hyperopt_config["search_alg"] = {}
else:
hyperopt_config["search_alg"] = {
TYPE: search_alg,
}
if validate_output_feature:
hyperopt_config["output_feature"] = config[OUTPUT_FEATURES][0][NAME]
if validation_metric:
hyperopt_config["validation_metric"] = validation_metric
update_hyperopt_params_with_defaults(hyperopt_config)
backend = initialize_backend("local")
if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO:
hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config)
parameters = hyperopt_config["parameters"]
output_feature = hyperopt_config["output_feature"]
metric = hyperopt_config["metric"]
goal = hyperopt_config["goal"]
executor = hyperopt_config["executor"]
search_alg = hyperopt_config["search_alg"]
hyperopt_executor = get_build_hyperopt_executor(RAY)(
parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor
)
results = hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir)
assert isinstance(results, HyperoptResults)
with hyperopt_executor._get_best_model_path(
results.experiment_analysis.best_trial, results.experiment_analysis
) as path:
assert path is not None
assert isinstance(path, str)
@pytest.mark.parametrize("model_type", [MODEL_ECD])
def test_hyperopt_executor_with_metric(model_type, csv_filename, tmpdir, ray_cluster_7cpu):
test_hyperopt_search_alg(
"variant_generator",
model_type,
csv_filename,
tmpdir,
ray_cluster_7cpu,
validate_output_feature=True,
validation_metric=ACCURACY,
)
@pytest.mark.parametrize("split", [VALIDATION])
def test_hyperopt_with_split(split, csv_filename, tmpdir, ray_cluster_7cpu):
test_hyperopt_search_alg(
search_alg="variant_generator",
model_type=MODEL_ECD,
csv_filename=csv_filename,
tmpdir=tmpdir,
ray_cluster_7cpu=ray_cluster_7cpu,
split=split,
)
@pytest.mark.parametrize("scheduler", SCHEDULERS_FOR_TESTING)
@pytest.mark.parametrize("model_type", [MODEL_ECD])
def test_hyperopt_scheduler(
scheduler, model_type, csv_filename, tmpdir, ray_cluster_7cpu, validate_output_feature=False, validation_metric=None
):
config, rel_path = _setup_ludwig_config(csv_filename, model_type)
hyperopt_config = HYPEROPT_CONFIG.copy()
# finalize hyperopt config settings
if scheduler == "pb2":
# setup scheduler hyperparam_bounds parameter
min = hyperopt_config["parameters"]["trainer.learning_rate"]["lower"]
max = hyperopt_config["parameters"]["trainer.learning_rate"]["upper"]
hyperparam_bounds = {
"trainer.learning_rate": [min, max],
}
hyperopt_config["executor"]["scheduler"] = {
TYPE: scheduler,
"hyperparam_bounds": hyperparam_bounds,
}
else:
hyperopt_config["executor"]["scheduler"] = {
TYPE: scheduler,
}
if validate_output_feature:
hyperopt_config["output_feature"] = config[OUTPUT_FEATURES][0][NAME]
if validation_metric:
hyperopt_config["validation_metric"] = validation_metric
backend = initialize_backend("local")
update_hyperopt_params_with_defaults(hyperopt_config)
if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO:
hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config)
parameters = hyperopt_config["parameters"]
split = hyperopt_config["split"]
output_feature = hyperopt_config["output_feature"]
metric = hyperopt_config["metric"]
goal = hyperopt_config["goal"]
executor = hyperopt_config["executor"]
search_alg = hyperopt_config["search_alg"]
# TODO: Determine if we still need this if-then-else construct
if search_alg[TYPE] in {""}:
with pytest.raises(ImportError):
get_build_hyperopt_executor(RAY)(
parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor
)
else:
hyperopt_executor = get_build_hyperopt_executor(RAY)(
parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor
)
raytune_results = hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir)
assert isinstance(raytune_results, HyperoptResults)
def _run_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, backend, ray_cluster_7cpu):
input_features = [category_feature(encoder={"vocab_size": 3})]
output_features = [category_feature(decoder={"vocab_size": 3})]
rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
COMBINER: {TYPE: "concat"},
TRAINER: {"epochs": 1, "learning_rate": 0.001, BATCH_SIZE: 128},
"backend": backend,
}
output_feature_name = output_features[0][NAME]
if search_space == "random":
# random search will be size of num_samples
search_parameters = {
"trainer.learning_rate": {
"lower": 0.0001,
"upper": 0.01,
"space": "loguniform",
},
output_feature_name + ".decoder.fc_layers": {
"space": "choice",
"categories": [
[{"output_size": 8}, {"output_size": 4}],
[{"output_size": 8}],
[{"output_size": 4}],
],
},
output_feature_name + ".decoder.fc_output_size": {"space": "choice", "categories": [4, 8, 12]},
}
else:
# grid search space will be product each parameter size
search_parameters = {
"trainer.learning_rate": {"space": "grid_search", "values": [0.001, 0.01]},
output_feature_name + ".decoder.fc_output_size": {"space": "grid_search", "values": [4, 8]},
}
hyperopt_configs = {
"parameters": search_parameters,
"goal": "minimize",
"output_feature": output_feature_name,
"validation_metrics": "loss",
"executor": {
TYPE: "ray",
"num_samples": 1 if search_space == "grid" else RANDOM_SEARCH_SIZE,
"max_concurrent_trials": 1,
},
"search_alg": {TYPE: "variant_generator"},
}
# add hyperopt parameter space to the config
config[HYPEROPT] = hyperopt_configs
experiment_name = f"test_hyperopt_{uuid.uuid4().hex}"
hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name)
if search_space == "random":
assert hyperopt_results.experiment_analysis.results_df.shape[0] == RANDOM_SEARCH_SIZE
else:
# compute size of search space for grid search
grid_search_size = 1
for k, v in search_parameters.items():
grid_search_size *= len(v["values"])
assert hyperopt_results.experiment_analysis.results_df.shape[0] == grid_search_size
# check for return results
assert isinstance(hyperopt_results, HyperoptResults)
# check for existence of the hyperopt statistics file
with use_credentials(minio_test_creds()):
assert fs_utils.path_exists(os.path.join(tmpdir, experiment_name, HYPEROPT_STATISTICS_FILE_NAME))
for trial in hyperopt_results.experiment_analysis.trials:
assert fs_utils.path_exists(
os.path.join(tmpdir, experiment_name, f"trial_{trial.trial_id}"),
)
# Verify best trial has a valid checkpoint
best_trial = hyperopt_results.experiment_analysis.best_trial
assert best_trial is not None
@pytest.mark.slow
@pytest.mark.parametrize("search_space", ["random", "grid"])
def test_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, ray_cluster_7cpu):
_run_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, "local", ray_cluster_7cpu)
@pytest.mark.xfail(
reason="PyArrow S3 C++ client uses chunked transfer encoding for multipart uploads, "
"which MinIO rejects with HTTP 411 MissingContentLength. Requires real AWS S3.",
strict=False,
)
def test_hyperopt_sync_remote(csv_filename, ray_cluster_7cpu, monkeypatch):
"""Test hyperopt with remote S3 (MinIO) storage for trial results."""
# Override AWS env vars so PyArrow's S3 client (used by Ray Tune internally)
# connects to MinIO instead of real AWS S3
minio_endpoint = os.environ.get("LUDWIG_MINIO_ENDPOINT", "http://localhost:9000")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", os.environ.get("LUDWIG_MINIO_ACCESS_KEY", "minio"))
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", os.environ.get("LUDWIG_MINIO_SECRET_KEY", "minio123"))
monkeypatch.setenv("AWS_ENDPOINT_URL", minio_endpoint)
monkeypatch.setenv("AWS_EC2_METADATA_DISABLED", "true")
backend = {
"type": "local",
"credentials": {
"artifacts": minio_test_creds(),
},
}
with remote_tmpdir("s3", "test") as tmpdir:
_run_hyperopt_run_hyperopt(
csv_filename,
"random",
tmpdir,
backend,
ray_cluster_7cpu,
)
def test_hyperopt_with_feature_specific_parameters(csv_filename, tmpdir, ray_cluster_7cpu):
input_features = [
text_feature(name="utterance", reduce_output="sum"),
category_feature(vocab_size=3),
]
output_features = [category_feature(vocab_size=3, output_feature=True)]
rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30)
filter_size_search_space = [5, 7]
embedding_size_search_space = [4, 8, 12]
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
COMBINER: {TYPE: "concat", "num_fc_layers": 2},
TRAINER: {"epochs": 1, "learning_rate": 0.001, BATCH_SIZE: 128},
HYPEROPT: {
"parameters": {
input_features[0][NAME] + ".encoder.filter_size": {
"space": "choice",
"categories": filter_size_search_space,
},
input_features[1][NAME] + ".encoder.embedding_size": {
"space": "choice",
"categories": embedding_size_search_space,
},
},
"goal": "minimize",
"output_feature": output_features[0][NAME],
"validation_metrics": "loss",
"executor": {TYPE: "ray", "num_samples": 1},
"search_alg": {TYPE: "variant_generator"},
},
}
hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name="test_hyperopt")
hyperopt_results_df = hyperopt_results.experiment_analysis.results_df
model_parameters = json.load(
open(
os.path.join(
hyperopt_results_df.iloc[0]["trial_dir"],
"test_hyperopt_run",
MODEL_FILE_NAME,
"model_hyperparameters.json",
)
)
)
for input_feature in model_parameters[INPUT_FEATURES]:
if input_feature[TYPE] == TEXT:
assert input_feature["encoder"]["filter_size"] in filter_size_search_space
elif input_feature[TYPE] == CATEGORY:
assert input_feature["encoder"]["embedding_size"] in embedding_size_search_space
def test_hyperopt_old_config(csv_filename, tmpdir, ray_cluster_7cpu):
old_config = {
"ludwig_version": "0.4",
INPUT_FEATURES: [
{"name": "cat1", TYPE: "category", "encoder": {"vocab_size": 2}},
{"name": "num1", TYPE: "number"},
],
OUTPUT_FEATURES: [
{"name": "bin1", TYPE: "binary"},
],
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
HYPEROPT: {
EXECUTOR: {
TYPE: "ray",
"time_budget_s": 200,
"cpu_resources_per_trial": 1,
},
"sampler": {
TYPE: "ray",
"scheduler": {
TYPE: "async_hyperband",
"max_t": 200,
"time_attr": "time_total_s",
"grace_period": 72,
"reduction_factor": 5,
},
"search_alg": {
TYPE: "variant_generator",
},
"num_samples": 2,
},
"parameters": {
"trainer.batch_size": {
"space": "choice",
"categories": [64, 128, 256],
},
"trainer.learning_rate": {
"space": "loguniform",
"lower": 0.001,
"upper": 0.1,
},
},
},
}
input_features = old_config[INPUT_FEATURES]
output_features = old_config[OUTPUT_FEATURES]
rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30)
hyperopt(old_config, dataset=rel_path, output_directory=tmpdir, experiment_name="test_hyperopt")
def test_hyperopt_nested_parameters(csv_filename, tmpdir, ray_cluster_7cpu):
config = {
INPUT_FEATURES: [
{"name": "cat1", TYPE: "category", "encoder": {"vocab_size": 2}},
{"name": "num1", TYPE: "number"},
],
OUTPUT_FEATURES: [
{"name": "bin1", TYPE: "binary"},
],
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
HYPEROPT: {
EXECUTOR: {
TYPE: "ray",
"time_budget_s": 200,
"cpu_resources_per_trial": 1,
"num_samples": 2,
"scheduler": {TYPE: "fifo"},
},
"search_alg": {TYPE: "variant_generator"},
"parameters": {
".": {
"space": "choice",
"categories": [
{
"combiner": {
"type": "tabnet",
"bn_virtual_bs": 32,
},
"trainer": {
"learning_rate_scaling": "sqrt",
"learning_rate_scheduler": {
"decay": "exponential",
"decay_steps": 20000,
"decay_rate": 0.8,
},
"optimizer": {"type": "adam"},
},
},
{
"combiner": {"type": "concat"},
"trainer": {"learning_rate_scaling": "linear"},
},
],
},
"trainer.learning_rate": {"space": "choice", "categories": [0.7, 0.42]},
},
},
}
input_features = config[INPUT_FEATURES]
output_features = config[OUTPUT_FEATURES]
rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30)
results = hyperopt(
config,
dataset=rel_path,
output_directory=tmpdir,
experiment_name="test_hyperopt_nested_params",
)
results_df = results.experiment_analysis.results_df
assert len(results_df) == 2
for _, trial_meta in results_df.iterrows():
trial_dir = trial_meta["trial_dir"]
trial_config = load_json(
os.path.join(trial_dir, "test_hyperopt_nested_params_run", MODEL_FILE_NAME, "model_hyperparameters.json")
)
assert len(trial_config[INPUT_FEATURES]) == len(config[INPUT_FEATURES])
assert len(trial_config[OUTPUT_FEATURES]) == len(config[OUTPUT_FEATURES])
assert trial_config[COMBINER][TYPE] in {"tabnet", "concat"}
if trial_config[COMBINER][TYPE] == "tabnet":
assert trial_config[COMBINER]["bn_virtual_bs"] == 32
assert trial_config[TRAINER]["learning_rate_scaling"] == "sqrt"
assert trial_config[TRAINER]["learning_rate_scheduler"]["decay"] == "exponential"
assert trial_config[TRAINER]["learning_rate_scheduler"]["decay_steps"] == 20000
assert trial_config[TRAINER]["learning_rate_scheduler"]["decay_rate"] == 0.8
assert trial_config[TRAINER]["optimizer"]["type"] == "adam"
else:
assert trial_config[TRAINER]["learning_rate_scaling"] == "linear"
assert trial_config[TRAINER]["learning_rate"] in {0.7, 0.42}
@pytest.mark.slow
def test_hyperopt_without_config_defaults(csv_filename, tmpdir, ray_cluster_7cpu):
input_features = [category_feature(encoder={"vocab_size": 3})]
output_features = [category_feature(decoder={"vocab_size": 3})]
rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
COMBINER: {TYPE: "concat"},
TRAINER: {"train_steps": 5, "learning_rate": 0.001, BATCH_SIZE: 128},
# Missing search_alg and executor, but should still work
HYPEROPT: {
"parameters": {
"trainer.learning_rate": {
"lower": 0.0001,
"upper": 0.01,
"space": "loguniform",
}
},
"goal": "minimize",
"output_feature": output_features[0]["name"],
"metric": "loss",
"executor": {"type": "ray", "num_samples": 2},
},
}
experiment_name = f"test_hyperopt_{uuid.uuid4().hex}"
hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name)
assert hyperopt_results.experiment_analysis.results_df.shape[0] == 2
@pytest.mark.slow
def test_hyperopt_with_time_budget(csv_filename, tmpdir, ray_cluster_7cpu):
"""Tests that incomplete checkpoints created by RayTune when time budget is hit doesn't throw errors because of
missing .tune_metadata files in the checkpoint directories."""
input_features = [text_feature()]
output_features = [category_feature(output_feature=True)]
rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
COMBINER: {TYPE: "concat"},
HYPEROPT: {
"goal": "minimize",
"metric": "loss",
"output_feature": output_features[0]["name"],
"search_alg": {TYPE: "variant_generator"},
"executor": {
"type": "ray",
# Ensure there is enough time for some trials to start and also for some to terminate
# to reproduce the exact issue of missing .tune_metadata files.
"time_budget_s": 30,
"cpu_resources_per_trial": 1,
"num_samples": 4,
"scheduler": {TYPE: "fifo"},
},
"parameters": {
"trainer.learning_rate": {
"lower": 0.0001,
"upper": 0.01,
"space": "loguniform",
}
},
},
}
experiment_name = f"test_hyperopt_{uuid.uuid4().hex}"
hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name)