From bd405f0ab2c7f5257f5e0490b546925e49caf7ec Mon Sep 17 00:00:00 2001 From: Franck Charras <29153872+fcharras@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:35:04 +0100 Subject: [PATCH] FEA ridge benchmarks (#18) Add benchmarks for Ridge. --------- Co-authored-by: Olivier Grisel --- ..._benchmark_results_file_sanity_checks.yaml | 1 + .../sync_benchmark_files_to_gsheet.yaml | 3 + .github/workflows/test_cpu_benchmarks.yaml | 2 + README.md | 1 + benchmarks/kmeans/consolidate_result_csv.py | 10 +- benchmarks/pca/consolidate_result_csv.py | 10 +- benchmarks/ridge/consolidate_result_csv.py | 643 ++++++++++++++++++ benchmarks/ridge/datasets/simulated_blobs.py | 42 ++ benchmarks/ridge/objective.py | 148 ++++ .../benchopt_run_2024-01-18_11h26m04.parquet | Bin 0 -> 34305 bytes .../benchopt_run_2024-01-18_12h59m51.parquet | Bin 0 -> 38350 bytes .../benchopt_run_2024-01-18_13h35m10.parquet | Bin 0 -> 34455 bytes .../benchopt_run_2024-01-18_16h06m03.parquet | Bin 0 -> 34395 bytes benchmarks/ridge/results.csv | 157 +++++ benchmarks/ridge/solvers/cuml.py | 107 +++ benchmarks/ridge/solvers/scikit_learn.py | 98 +++ .../ridge/solvers/scikit_learn_intelex.py | 148 ++++ .../ridge/solvers/sklearn_torch_dispatch.py | 140 ++++ 18 files changed, 1500 insertions(+), 10 deletions(-) create mode 100644 benchmarks/ridge/consolidate_result_csv.py create mode 100644 benchmarks/ridge/datasets/simulated_blobs.py create mode 100644 benchmarks/ridge/objective.py create mode 100644 benchmarks/ridge/outputs/benchopt_run_2024-01-18_11h26m04.parquet create mode 100644 benchmarks/ridge/outputs/benchopt_run_2024-01-18_12h59m51.parquet create mode 100644 benchmarks/ridge/outputs/benchopt_run_2024-01-18_13h35m10.parquet create mode 100644 benchmarks/ridge/outputs/benchopt_run_2024-01-18_16h06m03.parquet create mode 100644 benchmarks/ridge/results.csv create mode 100644 benchmarks/ridge/solvers/cuml.py create mode 100644 benchmarks/ridge/solvers/scikit_learn.py create mode 100644 benchmarks/ridge/solvers/scikit_learn_intelex.py create mode 100644 benchmarks/ridge/solvers/sklearn_torch_dispatch.py diff --git a/.github/workflows/run_benchmark_results_file_sanity_checks.yaml b/.github/workflows/run_benchmark_results_file_sanity_checks.yaml index d427868..a93e386 100644 --- a/.github/workflows/run_benchmark_results_file_sanity_checks.yaml +++ b/.github/workflows/run_benchmark_results_file_sanity_checks.yaml @@ -19,3 +19,4 @@ jobs: run: | python ./benchmarks/kmeans/consolidate_result_csv.py ./benchmarks/kmeans/results.csv --check-csv python ./benchmarks/pca/consolidate_result_csv.py ./benchmarks/pca/results.csv --check-csv + python ./benchmarks/ridge/consolidate_result_csv.py ./benchmarks/ridge/results.csv --check-csv diff --git a/.github/workflows/sync_benchmark_files_to_gsheet.yaml b/.github/workflows/sync_benchmark_files_to_gsheet.yaml index cbd2f25..c99700b 100644 --- a/.github/workflows/sync_benchmark_files_to_gsheet.yaml +++ b/.github/workflows/sync_benchmark_files_to_gsheet.yaml @@ -24,8 +24,11 @@ jobs: run: | python ./benchmarks/kmeans/consolidate_result_csv.py ./benchmarks/kmeans/results.csv --check-csv python ./benchmarks/pca/consolidate_result_csv.py ./benchmarks/pca/results.csv --check-csv + python ./benchmarks/ridge/consolidate_result_csv.py ./benchmarks/ridge/results.csv --check-csv echo "$GSPREAD_SERVICE_ACCOUNT_AUTH_KEY" > service_account.json python ./benchmarks/kmeans/consolidate_result_csv.py ./benchmarks/kmeans/results.csv \ --sync-to-gspread --gspread-url $GSPREAD_URL --gspread-auth-key ./service_account.json python ./benchmarks/pca/consolidate_result_csv.py ./benchmarks/pca/results.csv \ --sync-to-gspread --gspread-url $GSPREAD_URL --gspread-auth-key ./service_account.json + python ./benchmarks/ridge/consolidate_result_csv.py ./benchmarks/ridge/results.csv \ + --sync-to-gspread --gspread-url $GSPREAD_URL --gspread-auth-key ./service_account.json diff --git a/.github/workflows/test_cpu_benchmarks.yaml b/.github/workflows/test_cpu_benchmarks.yaml index 57b0707..4bb5cc5 100644 --- a/.github/workflows/test_cpu_benchmarks.yaml +++ b/.github/workflows/test_cpu_benchmarks.yaml @@ -143,3 +143,5 @@ jobs: PYTHONPATH=$PYTHONPATH:$(realpath ../../kmeans_dpcpp/) benchopt run --no-plot -l -d Simulated_correlated_data[n_samples=1000,n_features=14] cd ../pca benchopt run --no-plot -l -d Simulated_correlated_data[n_samples=100,n_features=100] + cd ../ridge + benchopt run --no-plot -l -d Simulated_correlated_data[n_samples=100,n_features=100,n_targets=2] diff --git a/README.md b/README.md index 82f5e88..214f473 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ hardware. Benchmarks are currently available for the following algorithms: - [k-means](https://github.com/soda-inria/sklearn-engine-benchmarks/tree/main/benchmarks/kmeans) - [PCA](https://github.com/soda-inria/sklearn-engine-benchmarks/tree/main/benchmarks/pca) +- [Ridge](https://github.com/soda-inria/sklearn-engine-benchmarks/tree/main/benchmarks/pca) Here is a (non-exhaustive) list of libraries that are compared in the benchmarks: - [scikit-learn](https://scikit-learn.org/stable/index.html) diff --git a/benchmarks/kmeans/consolidate_result_csv.py b/benchmarks/kmeans/consolidate_result_csv.py index 00ef838..5656264 100644 --- a/benchmarks/kmeans/consolidate_result_csv.py +++ b/benchmarks/kmeans/consolidate_result_csv.py @@ -2,7 +2,6 @@ from functools import partial from io import BytesIO from itertools import zip_longest -from operator import attrgetter import numpy as np import pandas as pd @@ -393,17 +392,18 @@ def _gspread_sync(source, gspread_url, gspread_auth_key): worksheet.freeze(0, 0) worksheet.resize(rows=n_rows + 1, cols=n_cols) worksheet.clear_notes(global_range) - white_background = dict( - backgroundColorStyle=dict(rgbColor=dict(red=1, green=1, blue=1, alpha=1)) + reset_format = dict( + backgroundColorStyle=dict(rgbColor=dict(red=1, green=1, blue=1, alpha=1)), + textFormat=dict(bold=False), ) - worksheet.format(global_range, white_background) + worksheet.format(global_range, reset_format) except gspread.WorksheetNotFound: worksheet = sheet.add_worksheet( GOOGLE_WORKSHEET_NAME, rows=n_rows + 1, cols=n_cols ) # ensure worksheets are sorted anti-alphabetically sheet.reorder_worksheets( - sorted(sheet.worksheets(), key=attrgetter("title"), reverse=True) + sorted(sheet.worksheets(), key=lambda worksheet: worksheet.title.lower()) ) # upload all values diff --git a/benchmarks/pca/consolidate_result_csv.py b/benchmarks/pca/consolidate_result_csv.py index e4e1a3f..90835e3 100644 --- a/benchmarks/pca/consolidate_result_csv.py +++ b/benchmarks/pca/consolidate_result_csv.py @@ -2,7 +2,6 @@ from functools import partial from io import BytesIO from itertools import zip_longest -from operator import attrgetter import numpy as np import pandas as pd @@ -391,17 +390,18 @@ def _gspread_sync(source, gspread_url, gspread_auth_key): worksheet.freeze(0, 0) worksheet.resize(rows=n_rows + 1, cols=n_cols) worksheet.clear_notes(global_range) - white_background = dict( - backgroundColorStyle=dict(rgbColor=dict(red=1, green=1, blue=1, alpha=1)) + reset_format = dict( + backgroundColorStyle=dict(rgbColor=dict(red=1, green=1, blue=1, alpha=1)), + textFormat=dict(bold=False), ) - worksheet.format(global_range, white_background) + worksheet.format(global_range, reset_format) except gspread.WorksheetNotFound: worksheet = sheet.add_worksheet( GOOGLE_WORKSHEET_NAME, rows=n_rows + 1, cols=n_cols ) # ensure worksheets are sorted anti-alphabetically sheet.reorder_worksheets( - sorted(sheet.worksheets(), key=attrgetter("title"), reverse=True) + sorted(sheet.worksheets(), key=lambda worksheet: worksheet.title.lower()) ) # upload all values diff --git a/benchmarks/ridge/consolidate_result_csv.py b/benchmarks/ridge/consolidate_result_csv.py new file mode 100644 index 0000000..892cbfd --- /dev/null +++ b/benchmarks/ridge/consolidate_result_csv.py @@ -0,0 +1,643 @@ +import hashlib +from functools import partial +from io import BytesIO +from itertools import zip_longest + +import numpy as np +import pandas as pd +from pandas.io.parsers.readers import STR_NA_VALUES + +GOOGLE_WORKSHEET_NAME = "Ridge" + +DATES_FORMAT = "%Y-%m-%d" + +BENCHMARK_DEFINING_COLUMNS = [ + "objective_objective_param___name", + "objective_dataset_param___name", + "objective_dataset_param_n_samples", + "objective_dataset_param_n_features", + "objective_dataset_param_n_targets", + "objective_dataset_param_dtype", + "objective_dataset_param_random_state", + "objective_objective_param_alpha", + "objective_objective_param_fit_intercept", + "objective_objective_param_sample_weight", + "objective_objective_param_random_state", +] + +BENCHMARK_DEFINING_COLUMNS = sorted(BENCHMARK_DEFINING_COLUMNS) +_benchmark_defining_columns_identifier = "".join(sorted(BENCHMARK_DEFINING_COLUMNS)) + +BACKEND_PROVIDER = "Backend provider" +COMMENT = "Comment" +COMPUTE_DEVICE = "Compute device" +COMPUTE_RUNTIME = "Compute runtime" +DATA_RANDOM_STATE = "Data random state" +REGULARIZATION_STRENGTH = "Regularization strength" +DTYPE = "Dtype" +NB_DATA_FEATURES = "Nb data features" +NB_DATA_SAMPLES = "Nb data samples" +NB_DATA_TARGETS = "Nb data targets" +SOLVER = "Solver" +PLATFORM = "Platform" +PLATFORM_ARCHITECTURE = "Platform architecture" +PLATFORM_RELEASE = "Platform release" +SYSTEM_CPUS = "Nb cpus" +SYSTEM_PROCESSOR = "Cpu name" +SYSTEM_RAM = "RAM (GB)" +SYSTEM_GPU = "Gpu name" +RESULT_NB_ITERATIONS = "Result nb iterations" +OBJECTIVE_FUNCTION_VALUE = "Result objective value" +VERSION_INFO = "Version info" +RUN_DATE = "Run date" +SOLVER_RANDOM_STATE = "Solver random state" +WALLTIME = "Walltime" + +BENCHMARK_ID_NAME = "Benchmark id" + +TABLE_DISPLAY_ORDER = [ + BENCHMARK_ID_NAME, + DTYPE, + NB_DATA_SAMPLES, + NB_DATA_FEATURES, + NB_DATA_TARGETS, + REGULARIZATION_STRENGTH, + WALLTIME, + BACKEND_PROVIDER, + COMPUTE_DEVICE, + COMPUTE_RUNTIME, + SOLVER, + SYSTEM_CPUS, + SYSTEM_PROCESSOR, + SYSTEM_GPU, + SYSTEM_RAM, + PLATFORM, + PLATFORM_ARCHITECTURE, + PLATFORM_RELEASE, + RUN_DATE, + VERSION_INFO, + COMMENT, + RESULT_NB_ITERATIONS, + OBJECTIVE_FUNCTION_VALUE, + DATA_RANDOM_STATE, + SOLVER_RANDOM_STATE, +] + +COLUMNS_DTYPES = { + BENCHMARK_ID_NAME: str, + DTYPE: str, + NB_DATA_SAMPLES: np.int64, + NB_DATA_FEATURES: np.int64, + NB_DATA_TARGETS: np.int64, + REGULARIZATION_STRENGTH: np.float64, + WALLTIME: np.float64, + BACKEND_PROVIDER: str, + COMPUTE_DEVICE: str, + COMPUTE_RUNTIME: str, + # NB: following should be int but str is more practical because it enables + # use of missing values for solver for which it doesn't apply. + RESULT_NB_ITERATIONS: str, + OBJECTIVE_FUNCTION_VALUE: np.float64, + SOLVER: str, + PLATFORM: str, + PLATFORM_ARCHITECTURE: str, + PLATFORM_RELEASE: str, + SYSTEM_CPUS: np.int64, + SYSTEM_PROCESSOR: str, + SYSTEM_GPU: str, + SYSTEM_RAM: np.int64, + DATA_RANDOM_STATE: np.int64, + SOLVER_RANDOM_STATE: np.int64, + VERSION_INFO: str, + RUN_DATE: str, + COMMENT: str, +} + +COLUMNS_WITH_NONE_STRING = [] + +# If all those fields have equal values for two given benchmarks, then the oldest +# benchmark (given by RUN_DATE) will be discarded +UNIQUE_BENCHMARK_KEY = [ + BENCHMARK_ID_NAME, + DTYPE, + NB_DATA_SAMPLES, + NB_DATA_FEATURES, + NB_DATA_TARGETS, + REGULARIZATION_STRENGTH, + BACKEND_PROVIDER, + SOLVER, + COMPUTE_DEVICE, + COMPUTE_RUNTIME, + PLATFORM, + PLATFORM_ARCHITECTURE, + SYSTEM_PROCESSOR, + SYSTEM_CPUS, + SYSTEM_GPU, + DATA_RANDOM_STATE, + SOLVER_RANDOM_STATE, +] + +# Importance and say if ascending / descending +ROW_SORT_ORDER = [ + (DTYPE, True), + (NB_DATA_SAMPLES, False), + (NB_DATA_FEATURES, False), + (NB_DATA_TARGETS, True), + (REGULARIZATION_STRENGTH, False), + (WALLTIME, True), + (BACKEND_PROVIDER, True), + (COMPUTE_DEVICE, True), + (COMPUTE_RUNTIME, True), + (RESULT_NB_ITERATIONS, True), + (OBJECTIVE_FUNCTION_VALUE, False), + (SOLVER, True), + (SYSTEM_GPU, True), + (SYSTEM_CPUS, True), + (PLATFORM, True), + (PLATFORM_ARCHITECTURE, True), + (PLATFORM_RELEASE, True), + (SYSTEM_PROCESSOR, True), + (SYSTEM_RAM, True), + (DATA_RANDOM_STATE, True), + (SOLVER_RANDOM_STATE, True), + (RUN_DATE, False), + (VERSION_INFO, False), + (COMMENT, True), + (BENCHMARK_ID_NAME, True), +] +_row_sort_by, _row_sort_ascending = map(list, zip(*ROW_SORT_ORDER)) + +PARQUET_TABLE_DISPLAY_MAPPING = dict( + time=WALLTIME, + objective_value=OBJECTIVE_FUNCTION_VALUE, + objective_n_iter=RESULT_NB_ITERATIONS, + objective_dataset_param_n_samples=NB_DATA_SAMPLES, + objective_dataset_param_n_features=NB_DATA_FEATURES, + objective_dataset_param_n_targets=NB_DATA_TARGETS, + objective_objective_param_alpha=REGULARIZATION_STRENGTH, + objective_dataset_param_dtype=DTYPE, + objective_dataset_param_random_state=DATA_RANDOM_STATE, + objective_objective_param_random_state=SOLVER_RANDOM_STATE, + objective_objective_param_solver=SOLVER, + objective_solver_param___name=BACKEND_PROVIDER, + objective_solver_param_device=COMPUTE_DEVICE, + objective_solver_param_runtime=COMPUTE_RUNTIME, + objective_solver_param_comment=COMMENT, + objective_solver_param_version_info=VERSION_INFO, + objective_solver_param_run_date=RUN_DATE, + platform=PLATFORM, +) + +PARQUET_TABLE_DISPLAY_MAPPING.update( + { + "platform-architecture": PLATFORM_ARCHITECTURE, + "platform-release": PLATFORM_RELEASE, + "system-cpus": SYSTEM_CPUS, + "system-processor": SYSTEM_PROCESSOR, + "system-ram (GB)": SYSTEM_RAM, + } +) +_all_table_columns = list(PARQUET_TABLE_DISPLAY_MAPPING) + [BENCHMARK_ID_NAME] + +ALL_EXPECTED_COLUMNS = set(BENCHMARK_DEFINING_COLUMNS + _all_table_columns) + +IDS_LENGTH = 8 + + +def _get_id_from_str(s): + return hashlib.sha256(s.encode("utf8"), usedforsecurity=False).hexdigest()[ + :IDS_LENGTH + ] + + +def _get_sample_id_for_columns(row, defining_colums, constant_identifier): + return _get_id_from_str( + "".join(row[defining_colums].astype(str)) + constant_identifier + ) + + +def _validate_one_parquet_table(source): + df = pd.read_parquet(source) + + # NB: we're lenient on the columns + for col in ALL_EXPECTED_COLUMNS - set(df.columns): + df[col] = None + + df[BENCHMARK_ID_NAME] = df.apply( + lambda row: _get_sample_id_for_columns( + row, BENCHMARK_DEFINING_COLUMNS, _benchmark_defining_columns_identifier + ), + axis=1, + ) + + df = df[_all_table_columns] + df.rename(columns=PARQUET_TABLE_DISPLAY_MAPPING, inplace=True, errors="raise") + + df[RUN_DATE] = df[RUN_DATE].astype("datetime64[ns]") + + return df + + +def _validate_one_csv_table(source, parse_dates=True, order_columns=True): + NA_VALUES = set(STR_NA_VALUES) + NA_VALUES.discard("None") + + df = pd.read_csv( + source, + usecols=TABLE_DISPLAY_ORDER, + dtype=COLUMNS_DTYPES, + index_col=False, + na_values={col: NA_VALUES for col in COLUMNS_WITH_NONE_STRING}, + keep_default_na=False, + ) + + if order_columns: + df = df[TABLE_DISPLAY_ORDER] + + if parse_dates: + df[RUN_DATE] = pd.to_datetime(df[RUN_DATE], format=DATES_FORMAT).astype( + "datetime64[ns]" + ) + + return df + + +def _assemble_output_table( + dfs_from_csv, dfs_from_parquet, parquet_gpu_name, create_gpu_entry, list_known_gpus +): + + if not list_known_gpus and (len(dfs_from_parquet) == 0): + if parquet_gpu_name is not None: + parameter_name = ( + "--parquet-gpu-name" if parquet_gpu_name else "--no-parquet-gpu-name" + ) + raise ValueError( + f"The parameter {parameter_name} should only be used if at least one " + "benchopt parquet table is being consolidated, but only got csv tables." + ) + if create_gpu_entry is not False: + raise ValueError( + "The parameter --create-gpu-entry should only be used if at least one " + "benchopt parquet table is being consolidated, but got only csv tables." + ) + elif not list_known_gpus and parquet_gpu_name is None: + raise ValueError( + "Please use the --parquet-gpu-name parameter to provide a gpu name that " + "will be added to the metadata of the samples in the input parquet tables " + "or use the --no-parquet-gpu-name if you intend to leave the corresponding " + "field empty." + ) + + else: + gpu_names_from_csv = set( + gpu_name + for df in dfs_from_csv + for gpu_name in df[SYSTEM_GPU] + if (len(gpu_name) > 0) + ) + + if list_known_gpus: + print("\n".join(gpu_names_from_csv)) + return False + + if ( + (len(parquet_gpu_name) > 0) + and (parquet_gpu_name not in gpu_names_from_csv) + and not create_gpu_entry + ): + raise IndexError( + f"The gpu name {parquet_gpu_name} is unknown. Please use the " + "--new-gpu-entry parameter to confirm the addition of the new gpu " + "entry in the output csv table, or use --list-known-gpus parameter to " + "print a list of gpus names that have been already registered and use " + "one of those to bypass this error." + ) + + for df in dfs_from_parquet: + df[SYSTEM_GPU] = parquet_gpu_name + + df_list = dfs_from_csv + dfs_from_parquet + + if len(df_list) > 1: + df = pd.concat(df_list, ignore_index=True, copy=False) + else: + df = df_list[0] + + df = df[TABLE_DISPLAY_ORDER] + df.sort_values( + by=_row_sort_by, ascending=_row_sort_ascending, inplace=True, kind="stable" + ) + # HACK: sanitize mix of None values and empty strings that can happen when some + # columns are missing in the parquet input files (because it's optional and no + # solver returns it in the batch) by passing the data to CSV and re-loading + # again from CSV + df = _sanitize_df_with_tocsv(df) + + df.drop_duplicates(subset=UNIQUE_BENCHMARK_KEY, inplace=True, ignore_index=True) + + return df + + +def _sanitize_df_with_tocsv(df): + in_memory_buffer = BytesIO() + _df_to_csv(df, in_memory_buffer) + in_memory_buffer.seek(0) + return _validate_one_csv_table(in_memory_buffer, order_columns=False) + + +def _df_to_csv(df, target): + float_format_fn = partial( + np.format_float_positional, + precision=3, + unique=True, + fractional=False, + trim="-", + sign=False, + pad_left=None, + pad_right=None, + min_digits=None, + ) + df = df.copy() + df[WALLTIME] = df[WALLTIME].map(float_format_fn) + df.to_csv( + target, + index=False, + mode="a", + date_format=DATES_FORMAT, + ) + + +def _gspread_sync(source, gspread_url, gspread_auth_key): + import gspread + + df = _validate_one_csv_table(source, parse_dates=False) + + n_rows, n_cols = df.shape + walltime_worksheet_col = df.columns.get_loc(WALLTIME) + 1 + + gs = gspread.service_account(gspread_auth_key) + sheet = gs.open_by_url(gspread_url) + + global_range = ( + f"{gspread.utils.rowcol_to_a1(1, 1)}:" + f"{gspread.utils.rowcol_to_a1(n_rows + 1, n_cols)}" + ) + + try: + worksheet = sheet.worksheet(GOOGLE_WORKSHEET_NAME) + worksheet.clear() + worksheet.clear_basic_filter() + worksheet.freeze(0, 0) + worksheet.resize(rows=n_rows + 1, cols=n_cols) + worksheet.clear_notes(global_range) + reset_format = dict( + backgroundColorStyle=dict(rgbColor=dict(red=1, green=1, blue=1, alpha=1)), + textFormat=dict(bold=False), + ) + worksheet.format(global_range, reset_format) + except gspread.WorksheetNotFound: + worksheet = sheet.add_worksheet( + GOOGLE_WORKSHEET_NAME, rows=n_rows + 1, cols=n_cols + ) + + # ensure worksheets are sorted anti-alphabetically + sheet.reorder_worksheets( + sorted(sheet.worksheets(), key=lambda worksheet: worksheet.title.lower()) + ) + + # upload all values + worksheet.update( + values=[df.columns.values.tolist()] + df.values.tolist(), range_name="A1" + ) + + # set filter + worksheet.set_basic_filter(1, 1, n_rows + 1, n_cols) + + # freeze filter rows and benchmark-defining cols + worksheet.freeze(rows=1, cols=walltime_worksheet_col) + + format_queries = [] + + # Text is centerd and wrapped in all cells + global_format = dict( + horizontalAlignment="CENTER", + verticalAlignment="MIDDLE", + wrapStrategy="WRAP", + ) + format_queries.append(dict(range=global_range, format=global_format)) + + # benchmark_id and walltime columns are bold + bold_format = dict(textFormat=dict(bold=True)) + benchmark_id_col_range = ( + f"{gspread.utils.rowcol_to_a1(2, 1)}:" + f"{gspread.utils.rowcol_to_a1(n_rows + 1, 1)}" + ) + walltime_col_range = ( + f"{gspread.utils.rowcol_to_a1(2, walltime_worksheet_col)}:" + f"{gspread.utils.rowcol_to_a1(n_rows + 1, walltime_worksheet_col)}" + ) + format_queries.append(dict(range=benchmark_id_col_range, format=bold_format)) + format_queries.append(dict(range=walltime_col_range, format=bold_format)) + + # Header is light-ish yellow + yellow_lighter_header = dict( + backgroundColorStyle=dict( + rgbColor=dict(red=1, green=1, blue=102 / 255, alpha=1) + ) + ) + header_row_range = ( + f"{gspread.utils.rowcol_to_a1(1, 1)}:" + f"{gspread.utils.rowcol_to_a1(1, n_cols)}" + ) + format_queries.append(dict(range=header_row_range, format=yellow_lighter_header)) + + # Every other benchmark_id has greyed background + bright_gray_background = dict( + backgroundColorStyle=dict( + rgbColor=dict(red=232 / 255, green=233 / 255, blue=235 / 255, alpha=1) + ) + ) + benchmark_ids = df[BENCHMARK_ID_NAME] + benchmark_ids_ending_idx = ( + np.where((benchmark_ids.shift() != benchmark_ids).values[1:])[0] + 2 + ) + for benchmark_id_range_start, benchmark_id_range_end in zip_longest( + *(iter(benchmark_ids_ending_idx),) * 2 + ): + benchmark_row_range = ( + f"{gspread.utils.rowcol_to_a1(benchmark_id_range_start + 1, 1)}:" + f"{gspread.utils.rowcol_to_a1(benchmark_id_range_end or (n_rows + 1), n_cols)}" # noqa + ) + format_queries.append( + dict(range=benchmark_row_range, format=bright_gray_background) + ) + + # Apply formats + worksheet.batch_format(format_queries) + + # auto-resize rows and cols + worksheet.columns_auto_resize(0, n_cols - 1) + worksheet.rows_auto_resize(0, n_rows) + + +if __name__ == "__main__": + import os + import sys + from argparse import ArgumentParser + + argparser = ArgumentParser( + description=( + "Print an aggregated CSV-formated database of ridge benchmark results " + "for the sklearn-engine-benchmarks project hosted at " + "https://github.com/soda-inria/sklearn-engine-benchmarks.\n\n" + "The inputs are assumed to be a collection of benchopt parquet files and " + "CSV files, well formated according to the project current specs. This " + "command assumes rhat the inputs are valid and is lenient at checking " + "types, null values, or missing columns, hence the user is advised to " + "cautiously check outputs before using.\n\n" + "If several results are found for identical benchmarks, only the most " + "recent `Run date` value is retained, all anterior entries are discarded " + "from the output CSV." + ) + ) + + argparser.add_argument( + "benchmark_files", + nargs="+", + help="benchopt parquet files or sklearn-engine-benchmarks csv files", + ) + + argparser.add_argument( + "--check-csv", + action="store_true", + help="Perform a few sanity checks on a CSV database of ridge benchmark " + "results. If this option is passed, then the command only expects a single " + "input path to a csv file.", + ) + + argparser.add_argument( + "--sync-to-gspread", + action="store_true", + help="Synchronize a CSV database of ridge benchmark results to a google " + "spreadsheet and format it nicely. If this option is passed, then the command " + "only expects a single input path to a csv file, and also requires " + "--gspread-url and --gspread-auth-key.", + ) + + argparser.add_argument( + "--gspread-url", + help="URL to a google spreadsheet. Expected if and only if --sync-to-gspread " + "is passed.", + ) + + argparser.add_argument( + "--gspread-auth-key", + help="Path to a json authentication key for a gspread service account. " + "Expected if and only if --sync-to-gspread is passed.", + ) + + argparser.add_argument( + "--parquet-gpu-name", + help="Name of the GPU on the host that runs the benchmarks that are recorded " + "in the input parquet files.", + ) + + argparser.add_argument( + "--no-parquet-gpu-name", + action="store_true", + help="Do not insert a GPU name in the metadata of the benchmark samples that " + "were recorded in the input parquet files (and leave it blank).", + ) + + argparser.add_argument( + "--new-gpu-entry", + action="store_true", + help="Use this parameter along with --parquet-gpu-name to confirm that if the " + "GPU name is not yet known in the existing databases, it will be added to the " + "list of known GPU names. Else the command will throw an error.", + ) + + argparser.add_argument( + "--list-known-gpus", + action="store_true", + help="Will print a list of the GPU names that are used in CSV benchmark files.", + ) + + args = argparser.parse_args() + + if (parquet_gpu_name := args.parquet_gpu_name) is None and args.no_parquet_gpu_name: + parquet_gpu_name = "" + + create_gpu_entry = args.new_gpu_entry + list_known_gpus = args.list_known_gpus + + paths = args.benchmark_files + if (check_csv := args.check_csv) or args.sync_to_gspread: + if (n_paths := len(paths)) > 1: + command = "--check-csv" if check_csv else "--sync-to-gspread" + raise ValueError( + f"A single input path to a csv file is expected when the {command} " + f"parameter is passed, but you passed {n_paths - 1} additional " + "arguments." + ) + path = paths[0] + _, file_extension = os.path.splitext(path) + if file_extension != ".csv": + raise ValueError( + "Expecting a '.csv' file extensions, but got " + f"{file_extension} instead !" + ) + + if check_csv: + df_loaded = _validate_one_csv_table(path) + df_clean = _assemble_output_table( + dfs_from_csv=[df_loaded], + dfs_from_parquet=[], + parquet_gpu_name=None, + create_gpu_entry=False, + list_known_gpus=list_known_gpus, + ) + + pd.testing.assert_frame_equal(df_loaded, df_clean) + + if gspread_sync := args.sync_to_gspread: + if (gspread_url := args.gspread_url) is None: + raise ValueError( + "Please provide a URL to a google spreadsheet using the " + "--gspread-url parameter." + ) + + if (gspread_auth_key := args.gspread_auth_key) is None: + raise ValueError( + "Please use the --gspread-auth-key parameter to pass a json " + "authentication key for a service account from the google developer " + "console." + ) + _gspread_sync(path, gspread_url, gspread_auth_key) + + if not check_csv and not gspread_sync: + dfs_from_parquet, dfs_from_csv = [], [] + for path in paths: + _, file_extension = os.path.splitext(path) + if file_extension == ".parquet": + if list_known_gpus: + continue + dfs_from_parquet.append(_validate_one_parquet_table(path)) + elif file_extension == ".csv": + dfs_from_csv.append(_validate_one_csv_table(path, order_columns=False)) + else: + raise ValueError( + "Expecting '.csv' or '.parquet' file extensions, but got " + f"{file_extension} instead !" + ) + + df = _assemble_output_table( + dfs_from_csv=dfs_from_csv, + dfs_from_parquet=dfs_from_parquet, + parquet_gpu_name=parquet_gpu_name, + create_gpu_entry=create_gpu_entry, + list_known_gpus=list_known_gpus, + ) + + if df is not False: + _df_to_csv(df, sys.stdout) diff --git a/benchmarks/ridge/datasets/simulated_blobs.py b/benchmarks/ridge/datasets/simulated_blobs.py new file mode 100644 index 0000000..d850f73 --- /dev/null +++ b/benchmarks/ridge/datasets/simulated_blobs.py @@ -0,0 +1,42 @@ +from benchopt import BaseDataset, safe_import_context +from benchopt.datasets import make_correlated_data + +with safe_import_context() as import_ctx: + import numpy as np + + +class Dataset(BaseDataset): + name = "Simulated_correlated_data" + + parameters = { + "n_samples, n_features": [ + (10_000_000, 100), + (5_000_000, 100), + (5_000_000, 10), + (5000, 10000), + (5000, 5000), + ], + "n_targets": [1, 10], + "dtype": ["float32"], + "random_state": [123], + } + + def __init__(self, n_samples, n_features, n_targets, dtype, random_state): + self.n_samples = n_samples + self.n_features = n_features + self.n_targets = n_targets + self.random_state = random_state + self.dtype = dtype + + def get_data(self): + rng = np.random.RandomState(self.random_state) + + X, y, _ = make_correlated_data( + self.n_samples, self.n_features, self.n_targets, random_state=rng + ) + + dtype = getattr(np, self.dtype) + + return dict( + X=X.astype(dtype), y=y.astype(dtype), __name=self.name, **self._parameters + ) diff --git a/benchmarks/ridge/objective.py b/benchmarks/ridge/objective.py new file mode 100644 index 0000000..f5520fc --- /dev/null +++ b/benchmarks/ridge/objective.py @@ -0,0 +1,148 @@ +import numbers +from datetime import datetime + +from benchopt import BaseObjective, safe_import_context + +with safe_import_context() as import_ctx: + import numpy as np + from sklearn.linear_model._base import _rescale_data + + +class Objective(BaseObjective): + name = "Ridge walltime" + url = "https://github.com/soda-inria/sklearn-engine-benchmarks" + + requirements = ["numpy"] + + # Since our goal is to measure walltime for solvers that perform exact same + # computations, the solver parameters are part of the objective and must be set + # for all solvers, rather than being an independent benchmark space for each + # solver. + parameters = { + "alpha": [1.0, 1e-10], + "fit_intercept": [True], + "solver, max_iter, tol": [ + ("svd", None, 0), + ("cholesky", None, 0), + ("lsqr", None, 1e-4), + ("sparse_cg", None, 1e-4), + ("sag", None, 1e-4), + ("saga", None, 1e-4), + ("cd", None, 1e-4), + ("eig", None, None), + # Used for scikit-learn-intelex that doesn't + # document the underlying solver nor expose the n_iter_ attribute + ("DefaultDense", None, 1e-4), + ], + "sample_weight": ["None"], # NB: add "random" to test non None weights + "random_state": [123], + } + + def set_data(self, X, y, **dataset_parameters): + self.X = X + self.y = y + dtype = X.dtype + + if self.sample_weight == "None": + sample_weight = None + elif self.sample_weight == "unary": + sample_weight = np.ones(len(X), dtype=dtype) + elif self.sample_weight == "random": + rng_sample_weight = np.random.default_rng( + dataset_parameters["random_state"] + 1 + ) + sample_weight = rng_sample_weight.random(size=len(X)).astype(dtype) + else: + raise ValueError( + "Expected 'sample_weight' parameter to be either equal to 'None', " + f"'unary' or 'random', but got {sample_weight}." + ) + + self.sample_weight_ = sample_weight + self.dataset_parameters = dataset_parameters + + def evaluate_result(self, weights, intercept, n_iter, **solver_parameters): + # NB: weights, intercept expected to be numpy arrays + + X, y = self.X, self.y + if self.sample_weight_ is not None: + X, y, _ = _rescale_data(X, y, self.sample_weight_, inplace=False) + + y = y.reshape((y.shape[0], -1)) + weights = weights.reshape((-1, X.shape[1], 1)) + + value = ( + (((X @ weights).squeeze(2) + (intercept - y).T) ** 2).sum() + + (self.alpha * (weights**2).sum()) + ) / (X.shape[0] * len(y.T)) + + all_parameters = dict(solver_param_run_date=datetime.today()) + all_parameters.update( + { + ("dataset_param_" + key): value + for key, value in self.dataset_parameters.items() + } + ) + all_parameters.update( + { + ("objective_param_" + key): value + for key, value in self._parameters.items() + } + ) + all_parameters.update( + {("solver_param_" + key): value for key, value in solver_parameters.items()} + ) + + if not (isinstance(n_iter, numbers.Number) or (n_iter is None)): + n_iter = max(n_iter) + + # NB: str for n_iter is a more practical type because it enables + # using missing values for solvers for which it doesn't apply + if n_iter is None: + n_iter = "" + else: + n_iter = str(n_iter) + + return dict( + value=value, + n_iter=n_iter, + objective_param___name=self.name, + **all_parameters, + ) + + def get_one_result(self): + n_features = self.dataset_parameters["n_features"] + n_targets = self.dataset_parameters["n_targets"] + if n_targets == 1: + weights = np.ones((n_features,)) + else: + weights = np.ones( + ( + n_targets, + n_features, + ) + ) + + return dict(weights=weights, intercept=np.ones((n_targets,))) + + def get_objective(self): + # Copy the data before sending to the solver, to ensure that no unfortunate + # side effects can happen + X = self.X.copy() + y = self.y.copy() + + sample_weight = self.sample_weight_ + if hasattr(sample_weight, "copy"): + sample_weight = sample_weight.copy() + + return dict( + X=X, + y=y, + sample_weight=sample_weight, + alpha=self.alpha, + fit_intercept=self.fit_intercept, + solver=self.solver, + max_iter=self.max_iter, + tol=self.tol, + random_state=self.random_state, + ) diff --git a/benchmarks/ridge/outputs/benchopt_run_2024-01-18_11h26m04.parquet b/benchmarks/ridge/outputs/benchopt_run_2024-01-18_11h26m04.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ca3adf5e9cc0d609952b1d89a7b078fa9428b6ec GIT binary patch literal 34305 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 bzT3nj@$d(q;jid#*1ybK6AUxN2OR$cx-95A literal 0 HcmV?d00001 diff --git a/benchmarks/ridge/outputs/benchopt_run_2024-01-18_12h59m51.parquet b/benchmarks/ridge/outputs/benchopt_run_2024-01-18_12h59m51.parquet new file mode 100644 index 0000000000000000000000000000000000000000..180efc8192d7e29e214fcb616685849746d6a232 GIT binary patch literal 38350 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 literal 0 HcmV?d00001 diff --git a/benchmarks/ridge/outputs/benchopt_run_2024-01-18_13h35m10.parquet b/benchmarks/ridge/outputs/benchopt_run_2024-01-18_13h35m10.parquet new file mode 100644 index 0000000000000000000000000000000000000000..25627ed8e226340a06375a4a95f4ba95958cfca0 GIT binary patch literal 34455 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 literal 0 HcmV?d00001 diff --git a/benchmarks/ridge/outputs/benchopt_run_2024-01-18_16h06m03.parquet b/benchmarks/ridge/outputs/benchopt_run_2024-01-18_16h06m03.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c1fd0685295c3fc1c1905450035889cb30b85ba2 GIT binary patch literal 34395 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 1): + return True, "Multitarget is not supported." + + solver = objective_dict["solver"] + # NB: should also support "cd" but it doesnt work + # TODO: investigate ? + if solver not in ["svd", "eig"]: + return True, "Only accepts the svd solver at the moment." + + return False, None + + def warm_up(self): + n_warmup_samples = 20 + n_warmup_features = 5 + sample_weight = self.sample_weight + if sample_weight is not None: + sample_weight = sample_weight[:n_warmup_samples].copy() + cuml.Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + solver=self.solver, + ).fit( + self.X[:n_warmup_samples, :n_warmup_features].copy(), + self.y[:n_warmup_samples].copy(), + sample_weight=sample_weight, + ) + + def run(self, _): + estimator = cuml.Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + solver=self.solver, + ).fit(self.X, self.y, sample_weight=self.sample_weight) + + self.weights = estimator.coef_ + self.intercept = estimator.intercept_ + + def get_result(self): + return dict( + weights=cupy.asnumpy(self.weights), + intercept=cupy.asnumpy(self.intercept), + n_iter=None, + version_info=f"scikit-learn {version('scikit-learn')}", + __name=self.name, + **self._parameters, + ) diff --git a/benchmarks/ridge/solvers/scikit_learn.py b/benchmarks/ridge/solvers/scikit_learn.py new file mode 100644 index 0000000..14674a2 --- /dev/null +++ b/benchmarks/ridge/solvers/scikit_learn.py @@ -0,0 +1,98 @@ +from importlib.metadata import version + +from benchopt import BaseSolver, safe_import_context +from benchopt.stopping_criterion import SingleRunCriterion + +with safe_import_context() as import_ctx: + from sklearn.linear_model import Ridge + + +class Solver(BaseSolver): + name = "scikit-learn" + requirements = ["scikit-learn"] + + stopping_criterion = SingleRunCriterion(1) + + def set_objective( + self, + X, + y, + sample_weight, + alpha, + fit_intercept, + solver, + max_iter, + tol, + random_state, + ): + self.X = X + self.y = y + self.sample_weight = sample_weight + self.alpha = alpha + self.fit_intercept = fit_intercept + self.solver = solver + self.max_iter = max_iter + self.tol = tol + self.random_state = random_state + + def skip(self, **objective_dict): + solver = objective_dict["solver"] + + if solver in ["sag", "saga"]: + # TODO: investigate ? + return True, ( + "Preliminary testing show this solver is too slow to have relevance " + "in the benchmark." + ) + + if solver in ["DefaultDense", "eig", "cd"]: + return True, "No support for this solver parameter." + + return False, None + + def warm_up(self): + n_warmup_samples = 20 + n_warmup_features = 5 + sample_weight = self.sample_weight + if sample_weight is not None: + sample_weight = sample_weight[:n_warmup_samples].copy() + Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + copy_X=False, + max_iter=self.max_iter, + tol=self.tol, + solver=self.solver, + positive=True if (self.solver == "lbfgs") else False, + random_state=self.random_state, + ).fit( + self.X[:n_warmup_samples, :n_warmup_features].copy(), + self.y[:n_warmup_samples].copy(), + sample_weight, + ) + + def run(self, _): + estimator = Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + copy_X=False, + max_iter=self.max_iter, + tol=self.tol, + solver=self.solver, + positive=True if (self.solver == "lbfgs") else False, + random_state=self.random_state, + ).fit(self.X, self.y, self.sample_weight) + + self.weights = estimator.coef_ + self.intercept = estimator.intercept_ + self.n_iter_ = estimator.n_iter_ + + def get_result(self): + return dict( + weights=self.weights, + intercept=self.intercept, + n_iter=self.n_iter_, + version_info=f"scikit-learn {version('scikit-learn')}", + __name=self.name, + **self._parameters, + ) diff --git a/benchmarks/ridge/solvers/scikit_learn_intelex.py b/benchmarks/ridge/solvers/scikit_learn_intelex.py new file mode 100644 index 0000000..c5f697f --- /dev/null +++ b/benchmarks/ridge/solvers/scikit_learn_intelex.py @@ -0,0 +1,148 @@ +from contextlib import nullcontext +from importlib.metadata import version + +from benchopt import BaseSolver, safe_import_context +from benchopt.stopping_criterion import SingleRunCriterion + +with safe_import_context() as import_ctx: + # isort: off + import dpctl + import numpy as np + from sklearnex.linear_model import Ridge + from sklearnex import config_context + + # isort: on + + +class Solver(BaseSolver): + name = "scikit-learn-intelex" + + requirements = [ + "scikit-learn-intelex", + "dpcpp-cpp-rt", + ] + + parameters = { + "device, runtime": [ + ("cpu", None), # TODO: replace "None" with "opencl" if relevant + ("gpu", "level_zero"), + ], + } + + stopping_criterion = SingleRunCriterion(1) + + def set_objective( + self, + X, + y, + sample_weight, + alpha, + fit_intercept, + solver, + max_iter, + tol, + random_state, + ): + # TODO: the overhead of the copy of the data from host to device could be + # eliminated if scikit-learn-intelex could just take usm_ndarray objects as + # input and directly run compute with the underlying memory buffer. The + # documentation at + # https://intel.github.io/scikit-learn-intelex/latest/oneapi-gpu.html#device-offloading # noqa + # suggests that it is the intended behavior, however in practice + # scikit-learn-intelex currently always perform underlying copies + # under the hood no matter what, and sometimes fails at doing so. See e.g. + # issue at + # https://github.com/intel/scikit-learn-intelex/issues/1534#issuecomment-1766266299 # noqa + + # if self.runtime != "numpy": + # device = device = dpctl.SyclDevice(f"{self.runtime}:{self.device}") + # self.X = dpt.asarray(X, copy=True, device=device) + # else: + # self.X = X + + self.X = X + self.y = y + self.sample_weight = sample_weight + self.alpha = alpha + self.fit_intercept = fit_intercept + self.solver = solver + self.max_iter = max_iter + self.tol = tol + self.random_state = random_state + + def skip(self, **objective_dict): + if self.runtime is not None: + try: + device = dpctl.SyclDevice(f"{self.runtime}:{self.device}") + except Exception: + return ( + True, + f"{self.runtime} runtime not found for device {self.device}", + ) + + X = objective_dict["X"] + if (X.dtype == np.float64) and not device.has_aspect_fp64: + return True, ( + f"This {self.device} device has no support for float64 compute" + ) + + solver = objective_dict["solver"] + + if solver != "DefaultDense": + # TODO: investigate ? + return True, "The only supported solver parameter is DefaultDense." + + return False, None + + def warm_up(self): + n_warmup_samples = 20 + n_warmup_features = 5 + sample_weight = self.sample_weight + if sample_weight is not None: + sample_weight = sample_weight[:n_warmup_samples].copy() + with nullcontext() if (self.runtime is None) else config_context( + target_offload=f"{self.runtime}:{self.device}" + ): + Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + copy_X=False, + max_iter=self.max_iter, + tol=self.tol, + solver="auto", + positive=True if (self.solver == "lbfgs") else False, + random_state=self.random_state, + ).fit( + self.X[:n_warmup_samples, :n_warmup_features].copy(), + self.y[:n_warmup_samples].copy(), + sample_weight, + ) + + def run(self, _): + with nullcontext() if (self.runtime is None) else config_context( + target_offload=f"{self.runtime}:{self.device}" + ): + estimator = Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + copy_X=False, + max_iter=self.max_iter, + tol=self.tol, + solver="auto", + positive=True if (self.solver == "lbfgs") else False, + random_state=self.random_state, + ).fit(self.X, self.y, self.sample_weight) + + self.weights = estimator.coef_ + self.intercept = estimator.intercept_ + self.n_iter_ = estimator.n_iter_ + + def get_result(self): + return dict( + weights=self.weights, + intercept=self.intercept, + n_iter=self.n_iter_, + version_info=f"scikit-learn-intelex {version('scikit-learn-intelex')}", + __name=self.name, + **self._parameters, + ) diff --git a/benchmarks/ridge/solvers/sklearn_torch_dispatch.py b/benchmarks/ridge/solvers/sklearn_torch_dispatch.py new file mode 100644 index 0000000..1ba6213 --- /dev/null +++ b/benchmarks/ridge/solvers/sklearn_torch_dispatch.py @@ -0,0 +1,140 @@ +from importlib.metadata import PackageNotFoundError, version + +from benchopt import BaseSolver, safe_import_context +from benchopt.stopping_criterion import SingleRunCriterion + +with safe_import_context() as import_ctx: + # isort: off + import numpy as np + + # NB: even if it's not use for the compute we rely on the sklearn_pytorch_engine + # for the few utilities it contains e.g for loading torch with xpu support and + # checking for float64 compat. + # This import is necessary to pre-load torch extensions + import sklearn_pytorch_engine # noqa + import torch + from sklearn import config_context + from sklearn.linear_model import Ridge + from sklearn_pytorch_engine._utils import has_fp64_support + + # isort: on + + +class Solver(BaseSolver): + name = "sklearn-torch-dispatch" + requirements = ["scikit-learn", "sklearn-pytorch-engine"] + + parameters = { + "device": ["cpu", "xpu", "cuda", "mps"], + } + + stopping_criterion = SingleRunCriterion(1) + + def set_objective( + self, + X, + y, + sample_weight, + alpha, + fit_intercept, + solver, + max_iter, + tol, + random_state, + ): + # Copy the data before running the benchmark to ensure that no unfortunate side + # effects can happen + self.X = torch.asarray(X, copy=True, device=self.device) + self.y = torch.asarray(y, copy=True, device=self.device) + self.sample_weight = sample_weight + if sample_weight is not None: + self.sample_weight = torch.asarray( + sample_weight, copy=True, device=self.device + ) + + self.alpha = alpha + self.fit_intercept = fit_intercept + self.solver = solver + self.max_iter = max_iter + self.tol = tol + self.random_state = random_state + + def skip(self, **objective_dict): + if not Ridge()._get_tags()["array_api_support"]: + return True, ( + "Requires the development branch for Ridge support for Array API." + ) + + try: + torch.zeros(1, dtype=torch.float32, device=self.device) + except Exception: + return True, f"{self.device} compute backend for pytorch not found" + + X = objective_dict["X"] + if (X.dtype == np.float64) and not has_fp64_support(self.device): + return True, ( + f"This {self.device} device has no support for float64 compute" + ) + + solver = objective_dict["solver"] + if solver != "svd": + return True, "Only accepts the svd solver at the moment." + + return False, None + + def warm_up(self): + n_warmup_samples = 20 + n_warmup_features = 5 + sample_weight = self.sample_weight + if sample_weight is not None: + sample_weight = sample_weight[:n_warmup_samples].clone() + with config_context(array_api_dispatch=True): + Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + copy_X=False, + max_iter=self.max_iter, + tol=self.tol, + solver=self.solver, + positive=True if (self.solver == "lbfgs") else False, + random_state=self.random_state, + ).fit( + self.X[:n_warmup_samples, :n_warmup_features].clone(), + self.y[:n_warmup_samples].clone(), + sample_weight, + ) + + def run(self, _): + with config_context(array_api_dispatch=True): + estimator = Ridge( + alpha=self.alpha, + fit_intercept=self.fit_intercept, + copy_X=False, + max_iter=self.max_iter, + tol=self.tol, + solver=self.solver, + positive=True if (self.solver == "lbfgs") else False, + random_state=self.random_state, + ).fit(self.X, self.y, self.sample_weight) + + self.weights = estimator.coef_ + self.intercept = estimator.intercept_ + self.n_iter_ = estimator.n_iter_ + + def get_result(self): + version_info = ( + f"scikit-learn {version('scikit-learn')}; torch {version('torch')}" + ) + try: + version_info += f"; ipex {version('intel-extension-for-pytorch')}" + except PackageNotFoundError: + pass + + return dict( + weights=self.weights.cpu().numpy(), + intercept=self.intercept.cpu().numpy(), + n_iter=self.n_iter_, + version_info=version_info, + __name=self.name, + **self._parameters, + )