Files
2026-07-13 13:17:40 +08:00

247 lines
7.7 KiB
Python

import json
import numpy as np
import os
import pandas as pd
import time
from typing import Dict
import xgboost as xgb
import lightgbm as lgb
import ray
from ray import data
from ray.train.lightgbm import (
LightGBMTrainer,
RayTrainReportCallback as LightGBMReportCallback,
normalize_pandas_for_lightgbm,
)
from ray.train.xgboost import (
RayTrainReportCallback as XGBoostReportCallback,
XGBoostTrainer,
)
from ray.train import RunConfig, ScalingConfig
_TRAINING_TIME_THRESHOLD = 600
_PREDICTION_TIME_THRESHOLD = 450
_EXPERIMENT_PARAMS = {
"smoke_test": {
"data": (
"https://air-example-data-2.s3.us-west-2.amazonaws.com/"
"10G-xgboost-data.parquet/8034b2644a1d426d9be3bbfa78673dfa_000000.parquet"
),
"num_workers": 1,
"cpus_per_worker": 1,
},
"10G": {
"data": "s3://air-example-data-2/10G-xgboost-data.parquet/",
"num_workers": 1,
"cpus_per_worker": 12,
},
"100G": {
"data": "s3://air-example-data-2/100G-xgboost-data.parquet/",
"num_workers": 10,
"cpus_per_worker": 12,
},
}
class BasePredictor:
def __init__(self, report_callback_cls, result: ray.train.Result):
self.model = report_callback_cls.get_model(result.checkpoint)
def __call__(self, data):
raise NotImplementedError
class XGBoostPredictor(BasePredictor):
def __call__(self, data: pd.DataFrame) -> Dict[str, np.ndarray]:
dmatrix = xgb.DMatrix(data)
return {"predictions": self.model.predict(dmatrix)}
class LightGBMPredictor(BasePredictor):
def __call__(self, data: pd.DataFrame) -> Dict[str, np.ndarray]:
return {"predictions": self.model.predict(normalize_pandas_for_lightgbm(data))}
def xgboost_train_loop_function(config: Dict):
train_ds_iter = ray.train.get_dataset_shard("train")
train_df = train_ds_iter.materialize().to_pandas()
label_column, params = config["label_column"], config["params"]
train_X, train_y = train_df.drop(label_column, axis=1), train_df[label_column]
dtrain = xgb.DMatrix(train_X, label=train_y)
report_callback = config["report_callback_cls"]
xgb.train(
params,
dtrain=dtrain,
num_boost_round=10,
callbacks=[report_callback()],
)
def lightgbm_train_loop_function(config: Dict):
train_ds_iter = ray.train.get_dataset_shard("train")
train_df = normalize_pandas_for_lightgbm(train_ds_iter.materialize().to_pandas())
label_column, params = config["label_column"], config["params"]
train_X, train_y = train_df.drop(label_column, axis=1), train_df[label_column]
train_set = lgb.Dataset(train_X, label=train_y)
report_callback = config["report_callback_cls"]
network_params = ray.train.lightgbm.get_network_params()
params.update(network_params)
lgb.train(
params,
train_set=train_set,
num_boost_round=10,
callbacks=[report_callback()],
)
_FRAMEWORK_PARAMS = {
"xgboost": {
"trainer_cls": XGBoostTrainer,
"predictor_cls": XGBoostPredictor,
"train_loop_function": xgboost_train_loop_function,
"train_loop_config": {
"params": {
"objective": "binary:logistic",
"eval_metric": ["logloss", "error"],
},
"label_column": "labels",
"report_callback_cls": XGBoostReportCallback,
},
},
"lightgbm": {
"trainer_cls": LightGBMTrainer,
"predictor_cls": LightGBMPredictor,
"train_loop_function": lightgbm_train_loop_function,
"train_loop_config": {
"params": {
"objective": "binary",
"metric": ["binary_logloss", "binary_error"],
},
"label_column": "labels",
"report_callback_cls": LightGBMReportCallback,
},
},
}
def train(
framework: str, data_path: str, num_workers: int, cpus_per_worker: int
) -> ray.train.Result:
ds = data.read_parquet(data_path)
framework_params = _FRAMEWORK_PARAMS[framework]
trainer_cls = framework_params["trainer_cls"]
framework_train_loop_fn = framework_params["train_loop_function"]
trainer = trainer_cls(
train_loop_per_worker=framework_train_loop_fn,
train_loop_config=framework_params["train_loop_config"],
scaling_config=ScalingConfig(
num_workers=num_workers,
resources_per_worker={"CPU": cpus_per_worker},
),
datasets={"train": ds},
run_config=RunConfig(
storage_path="/mnt/cluster_storage", name=f"{framework}_benchmark"
),
)
result = trainer.fit()
return result
def predict(framework: str, result: ray.train.Result, data_path: str):
framework_params = _FRAMEWORK_PARAMS[framework]
predictor_cls = framework_params["predictor_cls"]
ds = data.read_parquet(data_path)
ds = ds.drop_columns(["labels"])
concurrency = int(ray.cluster_resources()["CPU"] // 2)
ds.map_batches(
predictor_cls,
# Improve prediction throughput with larger batch size than default 4096
batch_size=8192,
concurrency=concurrency,
fn_constructor_kwargs={
"report_callback_cls": framework_params["train_loop_config"][
"report_callback_cls"
],
"result": result,
},
batch_format="pandas",
).write_parquet("/mnt/cluster_storage/predictions")
def main(args):
framework = args.framework
experiment = args.size if not args.smoke_test else "smoke_test"
experiment_params = _EXPERIMENT_PARAMS[experiment]
data_path, num_workers, cpus_per_worker = (
experiment_params["data"],
experiment_params["num_workers"],
experiment_params["cpus_per_worker"],
)
print(f"Running {framework} training benchmark...")
training_start = time.perf_counter()
result = train(framework, data_path, num_workers, cpus_per_worker)
training_time = time.perf_counter() - training_start
print(f"Running {framework} prediction benchmark...")
prediction_start = time.perf_counter()
predict(framework, result, data_path)
prediction_time = time.perf_counter() - prediction_start
times = {"training_time": training_time, "prediction_time": prediction_time}
print("Training result:\n", result)
print("Training/prediction times:", times)
test_output_json = os.environ.get("TEST_OUTPUT_JSON", "/tmp/result.json")
with open(test_output_json, "wt") as f:
json.dump(times, f)
if not args.disable_check:
if training_time > _TRAINING_TIME_THRESHOLD:
raise RuntimeError(
f"Training is taking {training_time} seconds, "
f"which is longer than expected ({_TRAINING_TIME_THRESHOLD} seconds)."
)
if prediction_time > _PREDICTION_TIME_THRESHOLD:
raise RuntimeError(
f"Batch prediction is taking {prediction_time} seconds, "
f"which is longer than expected ({_PREDICTION_TIME_THRESHOLD} seconds)."
)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"framework", type=str, choices=["xgboost", "lightgbm"], default="xgboost"
)
parser.add_argument("--size", type=str, choices=["10G", "100G"], default="100G")
# Add a flag for disabling the timeout error.
# Use case: running the benchmark as a documented example, in infra settings
# different from the formal benchmark's EC2 setup.
parser.add_argument(
"--disable-check",
action="store_true",
help="disable runtime error on benchmark timeout",
)
parser.add_argument("--smoke-test", action="store_true")
args = parser.parse_args()
main(args)