diff --git a/lib/LinearSolveAutotune/src/LinearSolveAutotune.jl b/lib/LinearSolveAutotune/src/LinearSolveAutotune.jl index 1e4e25507..53624d891 100644 --- a/lib/LinearSolveAutotune/src/LinearSolveAutotune.jl +++ b/lib/LinearSolveAutotune/src/LinearSolveAutotune.jl @@ -345,7 +345,7 @@ function autotune_setup(; # Set preferences if requested if set_preferences && !isempty(categories) - set_algorithm_preferences(categories) + set_algorithm_preferences(categories, results_df) end @info "Autotune setup completed!" diff --git a/lib/LinearSolveAutotune/src/preferences.jl b/lib/LinearSolveAutotune/src/preferences.jl index efc171bbb..6e9a1c334 100644 --- a/lib/LinearSolveAutotune/src/preferences.jl +++ b/lib/LinearSolveAutotune/src/preferences.jl @@ -1,38 +1,116 @@ # Preferences management for storing optimal algorithms in LinearSolve.jl """ - set_algorithm_preferences(categories::Dict{String, String}) + is_always_loaded_algorithm(algorithm_name::String) + +Determine if an algorithm is always loaded (available without extensions). +Returns true for algorithms that don't require extensions to be available. +""" +function is_always_loaded_algorithm(algorithm_name::String) + # Algorithms that are always available without requiring extensions + always_loaded = [ + "LUFactorization", + "GenericLUFactorization", + "MKLLUFactorization", # Available if MKL is loaded + "AppleAccelerateLUFactorization", # Available on macOS + "SimpleLUFactorization" + ] + + return algorithm_name in always_loaded +end + +""" + find_best_always_loaded_algorithm(results_df::DataFrame, eltype_str::String, size_range_name::String) + +Find the best always-loaded algorithm from benchmark results for a specific element type and size range. +Returns the algorithm name or nothing if no suitable algorithm is found. +""" +function find_best_always_loaded_algorithm(results_df::DataFrame, eltype_str::String, size_range_name::String) + # Define size ranges to match the categories + size_ranges = Dict( + "tiny (5-20)" => 5:20, + "small (20-100)" => 21:100, + "medium (100-300)" => 101:300, + "large (300-1000)" => 301:1000, + "big (1000+)" => 1000:typemax(Int) + ) + + size_range = get(size_ranges, size_range_name, nothing) + if size_range === nothing + @debug "Unknown size range: $size_range_name" + return nothing + end + + # Filter results for this element type and size range + filtered_results = filter( + row -> row.eltype == eltype_str && + row.size in size_range && + row.success && + !isnan(row.gflops) && + is_always_loaded_algorithm(row.algorithm), + results_df) + + if nrow(filtered_results) == 0 + return nothing + end + + # Calculate average GFLOPs for each always-loaded algorithm + avg_results = combine(groupby(filtered_results, :algorithm), + :gflops => (x -> mean(filter(!isnan, x))) => :avg_gflops) + + # Sort by performance and return the best + sort!(avg_results, :avg_gflops, rev = true) + + if nrow(avg_results) > 0 + return avg_results.algorithm[1] + end + + return nothing +end + +""" + set_algorithm_preferences(categories::Dict{String, String}, results_df::Union{DataFrame, Nothing} = nothing) Set LinearSolve preferences based on the categorized benchmark results. These preferences are stored in the main LinearSolve.jl package. +This function now supports the dual preference system introduced in LinearSolve.jl v2.31+: + + - `best_algorithm_{type}_{size}`: Overall fastest algorithm + - `best_always_loaded_{type}_{size}`: Fastest among always-available methods + The function handles type fallbacks: -- If Float32 wasn't benchmarked, uses Float64 results -- If ComplexF64 wasn't benchmarked, uses ComplexF32 results (if available) or Float64 -- If ComplexF32 wasn't benchmarked, uses Float64 results -- For complex types, avoids RFLUFactorization due to known issues + + - If Float32 wasn't benchmarked, uses Float64 results + - If ComplexF64 wasn't benchmarked, uses ComplexF32 results (if available) or Float64 + - If ComplexF32 wasn't benchmarked, uses Float64 results + - For complex types, avoids RFLUFactorization due to known issues + +If results_df is provided, it will be used to determine the best always-loaded algorithm +from actual benchmark data. Otherwise, a fallback strategy is used. """ -function set_algorithm_preferences(categories::Dict{String, String}) - @info "Setting LinearSolve preferences based on benchmark results..." - +function set_algorithm_preferences( + categories::Dict{String, String}, results_df::Union{DataFrame, Nothing} = nothing) + @info "Setting LinearSolve preferences based on benchmark results (dual preference system)..." + # Define the size category names we use size_categories = ["tiny", "small", "medium", "large", "big"] - + # Define the element types we want to set preferences for target_eltypes = ["Float32", "Float64", "ComplexF32", "ComplexF64"] - + # Extract benchmarked results by element type and size benchmarked = Dict{String, Dict{String, String}}() mkl_is_best_somewhere = false # Track if MKL wins any category - + for (key, algorithm) in categories if contains(key, "_") - eltype, size_range = split(key, "_", limit=2) + eltype, size_range = split(key, "_", limit = 2) if !haskey(benchmarked, eltype) benchmarked[eltype] = Dict{String, String}() end benchmarked[eltype][size_range] = algorithm - + # Check if MKL algorithm is best for this category if contains(algorithm, "MKL") mkl_is_best_somewhere = true @@ -40,7 +118,7 @@ function set_algorithm_preferences(categories::Dict{String, String}) end end end - + # Helper function to get best algorithm for complex types (avoiding RFLU) function get_complex_algorithm(results_df, eltype_str, size_range) # If we have direct benchmark results, use them @@ -57,90 +135,144 @@ function set_algorithm_preferences(categories::Dict{String, String}) end return nothing end - + # Process each target element type and size combination for eltype in target_eltypes for size_cat in size_categories - # Map size categories to the range strings used in categories - size_range = if size_cat == "tiny" - "0-128" # Maps to tiny range - elseif size_cat == "small" - "0-128" # Small also uses this range - elseif size_cat == "medium" - "128-256" # Medium range - elseif size_cat == "large" - "256-512" # Large range - elseif size_cat == "big" - "512+" # Big range - else - continue + # Find matching size range from benchmarked data for this element type + size_range = nothing + if haskey(benchmarked, eltype) + for range_key in keys(benchmarked[eltype]) + # Check if the range_key contains the size category we're looking for + # e.g., "medium (100-300)" contains "medium" + if contains(range_key, size_cat) + size_range = range_key + break + end + end + end + + if size_range === nothing + continue # No matching size range found for this element type and size category end - + # Determine the algorithm based on fallback rules algorithm = nothing - + if eltype == "Float64" # Float64 should be directly benchmarked - if haskey(benchmarked, "Float64") && haskey(benchmarked["Float64"], size_range) + if haskey(benchmarked, "Float64") && + haskey(benchmarked["Float64"], size_range) algorithm = benchmarked["Float64"][size_range] end elseif eltype == "Float32" # Float32: use Float32 results if available, else use Float64 - if haskey(benchmarked, "Float32") && haskey(benchmarked["Float32"], size_range) + if haskey(benchmarked, "Float32") && + haskey(benchmarked["Float32"], size_range) algorithm = benchmarked["Float32"][size_range] - elseif haskey(benchmarked, "Float64") && haskey(benchmarked["Float64"], size_range) + elseif haskey(benchmarked, "Float64") && + haskey(benchmarked["Float64"], size_range) algorithm = benchmarked["Float64"][size_range] end elseif eltype == "ComplexF32" # ComplexF32: use ComplexF32 if available, else Float64 (avoiding RFLU) - if haskey(benchmarked, "ComplexF32") && haskey(benchmarked["ComplexF32"], size_range) + if haskey(benchmarked, "ComplexF32") && + haskey(benchmarked["ComplexF32"], size_range) algorithm = benchmarked["ComplexF32"][size_range] - elseif haskey(benchmarked, "Float64") && haskey(benchmarked["Float64"], size_range) + elseif haskey(benchmarked, "Float64") && + haskey(benchmarked["Float64"], size_range) algorithm = benchmarked["Float64"][size_range] # Check for RFLU and warn - if contains(algorithm, "RFLU") || contains(algorithm, "RecursiveFactorization") + if contains(algorithm, "RFLU") || + contains(algorithm, "RecursiveFactorization") @warn "Would use RFLUFactorization for ComplexF32 at $size_cat, but it has issues with complex numbers. Consider benchmarking ComplexF32 directly." end end elseif eltype == "ComplexF64" # ComplexF64: use ComplexF64 if available, else ComplexF32, else Float64 (avoiding RFLU) - if haskey(benchmarked, "ComplexF64") && haskey(benchmarked["ComplexF64"], size_range) + if haskey(benchmarked, "ComplexF64") && + haskey(benchmarked["ComplexF64"], size_range) algorithm = benchmarked["ComplexF64"][size_range] - elseif haskey(benchmarked, "ComplexF32") && haskey(benchmarked["ComplexF32"], size_range) + elseif haskey(benchmarked, "ComplexF32") && + haskey(benchmarked["ComplexF32"], size_range) algorithm = benchmarked["ComplexF32"][size_range] - elseif haskey(benchmarked, "Float64") && haskey(benchmarked["Float64"], size_range) + elseif haskey(benchmarked, "Float64") && + haskey(benchmarked["Float64"], size_range) algorithm = benchmarked["Float64"][size_range] # Check for RFLU and warn - if contains(algorithm, "RFLU") || contains(algorithm, "RecursiveFactorization") + if contains(algorithm, "RFLU") || + contains(algorithm, "RecursiveFactorization") @warn "Would use RFLUFactorization for ComplexF64 at $size_cat, but it has issues with complex numbers. Consider benchmarking ComplexF64 directly." end end end - - # Set the preference if we have an algorithm + + # Set preferences if we have an algorithm if algorithm !== nothing - pref_key = "best_algorithm_$(eltype)_$(size_cat)" - Preferences.set_preferences!(LinearSolve, pref_key => algorithm; force = true) - @info "Set preference $pref_key = $algorithm in LinearSolve.jl" + # Set the best overall algorithm preference + best_pref_key = "best_algorithm_$(eltype)_$(size_cat)" + Preferences.set_preferences!(LinearSolve, best_pref_key => algorithm; force = true) + @info "Set preference $best_pref_key = $algorithm in LinearSolve.jl" + + # Determine the best always-loaded algorithm + best_always_loaded = nothing + + # If the best algorithm is already always-loaded, use it + if is_always_loaded_algorithm(algorithm) + best_always_loaded = algorithm + @info "Best algorithm ($algorithm) is always-loaded for $(eltype) $(size_cat)" + else + # Try to find the best always-loaded algorithm from benchmark results + if results_df !== nothing + best_always_loaded = find_best_always_loaded_algorithm(results_df, eltype, size_range) + if best_always_loaded !== nothing + @info "Found best always-loaded algorithm from benchmarks for $(eltype) $(size_cat): $best_always_loaded" + end + end + + # Fallback strategy if no benchmark data available or no suitable algorithm found + if best_always_loaded === nothing + if eltype == "Float64" || eltype == "Float32" + # For real types, prefer MKL > LU > Generic + if mkl_is_best_somewhere + best_always_loaded = "MKLLUFactorization" + else + best_always_loaded = "LUFactorization" + end + else + # For complex types, be more conservative since RFLU has issues + best_always_loaded = "LUFactorization" + end + @info "Using fallback always-loaded algorithm for $(eltype) $(size_cat): $best_always_loaded" + end + end + + # Set the best always-loaded algorithm preference + if best_always_loaded !== nothing + fallback_pref_key = "best_always_loaded_$(eltype)_$(size_cat)" + Preferences.set_preferences!( + LinearSolve, fallback_pref_key => best_always_loaded; force = true) + @info "Set preference $fallback_pref_key = $best_always_loaded in LinearSolve.jl" + end end end end - + # Set MKL preference based on whether it was best for any category # If MKL wasn't best anywhere, disable it to avoid loading unnecessary dependencies # Note: During benchmarking, MKL is temporarily enabled to test MKL algorithms # This final preference setting determines whether MKL loads in normal usage Preferences.set_preferences!(LinearSolve, "LoadMKL_JLL" => mkl_is_best_somewhere; force = true) - + if mkl_is_best_somewhere @info "MKL was best in at least one category - setting LoadMKL_JLL preference to true" else @info "MKL was not best in any category - setting LoadMKL_JLL preference to false to avoid loading unnecessary dependencies" end - + # Set a timestamp for when these preferences were created Preferences.set_preferences!(LinearSolve, "autotune_timestamp" => string(Dates.now()); force = true) - + @info "Preferences updated in LinearSolve.jl. You may need to restart Julia for changes to take effect." end @@ -148,26 +280,37 @@ end get_algorithm_preferences() Get the current algorithm preferences from LinearSolve.jl. -Returns preferences organized by element type and size category. +Returns preferences organized by element type and size category, including both +best overall and best always-loaded algorithms. """ function get_algorithm_preferences() - prefs = Dict{String, String}() - + prefs = Dict{String, Any}() + # Define the patterns we look for target_eltypes = ["Float32", "Float64", "ComplexF32", "ComplexF64"] size_categories = ["tiny", "small", "medium", "large", "big"] - + for eltype in target_eltypes for size_cat in size_categories - pref_key = "best_algorithm_$(eltype)_$(size_cat)" - value = Preferences.load_preference(LinearSolve, pref_key, nothing) - if value !== nothing - readable_key = "$(eltype)_$(size_cat)" - prefs[readable_key] = value + readable_key = "$(eltype)_$(size_cat)" + + # Get best overall algorithm + best_pref_key = "best_algorithm_$(eltype)_$(size_cat)" + best_value = Preferences.load_preference(LinearSolve, best_pref_key, nothing) + + # Get best always-loaded algorithm + fallback_pref_key = "best_always_loaded_$(eltype)_$(size_cat)" + fallback_value = Preferences.load_preference(LinearSolve, fallback_pref_key, nothing) + + if best_value !== nothing || fallback_value !== nothing + prefs[readable_key] = Dict( + "best" => best_value, + "always_loaded" => fallback_value + ) end end end - + return prefs end @@ -177,31 +320,39 @@ end Clear all autotune-related preferences from LinearSolve.jl. """ function clear_algorithm_preferences() - @info "Clearing LinearSolve autotune preferences..." - + @info "Clearing LinearSolve autotune preferences (dual preference system)..." + # Define the patterns we look for target_eltypes = ["Float32", "Float64", "ComplexF32", "ComplexF64"] size_categories = ["tiny", "small", "medium", "large", "big"] - + for eltype in target_eltypes for size_cat in size_categories - pref_key = "best_algorithm_$(eltype)_$(size_cat)" - if Preferences.has_preference(LinearSolve, pref_key) - Preferences.delete_preferences!(LinearSolve, pref_key; force = true) - @info "Cleared preference: $pref_key" + # Clear best overall algorithm preference + best_pref_key = "best_algorithm_$(eltype)_$(size_cat)" + if Preferences.has_preference(LinearSolve, best_pref_key) + Preferences.delete_preferences!(LinearSolve, best_pref_key; force = true) + @info "Cleared preference: $best_pref_key" + end + + # Clear best always-loaded algorithm preference + fallback_pref_key = "best_always_loaded_$(eltype)_$(size_cat)" + if Preferences.has_preference(LinearSolve, fallback_pref_key) + Preferences.delete_preferences!(LinearSolve, fallback_pref_key; force = true) + @info "Cleared preference: $fallback_pref_key" end end end - + # Clear timestamp if Preferences.has_preference(LinearSolve, "autotune_timestamp") Preferences.delete_preferences!(LinearSolve, "autotune_timestamp"; force = true) end - + # Clear MKL preference Preferences.delete_preferences!(LinearSolve, "LoadMKL_JLL"; force = true) @info "Cleared MKL preference" - + @info "Preferences cleared from LinearSolve.jl." end @@ -212,38 +363,50 @@ Display the current algorithm preferences from LinearSolve.jl in a readable form """ function show_current_preferences() prefs = get_algorithm_preferences() - + if isempty(prefs) println("No autotune preferences currently set in LinearSolve.jl.") return end - - println("Current LinearSolve.jl autotune preferences:") - println("="^50) - + + println("Current LinearSolve.jl autotune preferences (dual preference system):") + println("="^70) + # Group by element type for better display - by_eltype = Dict{String, Vector{Tuple{String, String}}}() - for (key, algorithm) in prefs - eltype, size_cat = split(key, "_", limit=2) + by_eltype = Dict{String, Vector{Tuple{String, Dict{String, Any}}}}() + for (key, pref_dict) in prefs + eltype, size_cat = split(key, "_", limit = 2) if !haskey(by_eltype, eltype) - by_eltype[eltype] = Vector{Tuple{String, String}}() + by_eltype[eltype] = Vector{Tuple{String, Dict{String, Any}}}() end - push!(by_eltype[eltype], (size_cat, algorithm)) + push!(by_eltype[eltype], (size_cat, pref_dict)) end - + for eltype in sort(collect(keys(by_eltype))) println("\n$eltype:") - for (size_cat, algorithm) in sort(by_eltype[eltype]) - println(" $size_cat: $algorithm") + for (size_cat, pref_dict) in sort(by_eltype[eltype]) + println(" $size_cat:") + best_alg = get(pref_dict, "best", nothing) + always_loaded_alg = get(pref_dict, "always_loaded", nothing) + + if best_alg !== nothing + println(" Best overall: $best_alg") + end + if always_loaded_alg !== nothing + println(" Best always-loaded: $always_loaded_alg") + end end end - + # Show MKL preference mkl_pref = Preferences.load_preference(LinearSolve, "LoadMKL_JLL", nothing) if mkl_pref !== nothing println("\nMKL Usage: $(mkl_pref ? "Enabled" : "Disabled")") end - + timestamp = Preferences.load_preference(LinearSolve, "autotune_timestamp", "unknown") println("\nLast updated: $timestamp") -end \ No newline at end of file + println("\nNOTE: This uses the enhanced dual preference system where LinearSolve.jl") + println("will try the best overall algorithm first, then fall back to the best") + println("always-loaded algorithm if extensions are not available.") +end diff --git a/lib/LinearSolveAutotune/test/runtests.jl b/lib/LinearSolveAutotune/test/runtests.jl index c8e64c88e..e4c7aef2d 100644 --- a/lib/LinearSolveAutotune/test/runtests.jl +++ b/lib/LinearSolveAutotune/test/runtests.jl @@ -204,33 +204,212 @@ if isempty(VERSION.prerelease) @test isa(system_info["has_metal"], Bool) end - @testset "Preference Management" begin - # Test setting and getting preferences with new format - test_categories = Dict{String, String}( - "Float64_0-128" => "TestAlg1", - "Float64_128-256" => "TestAlg2", - "Float32_0-128" => "TestAlg1" - ) + @testset "Algorithm Classification" begin + # Test is_always_loaded_algorithm function + @test LinearSolveAutotune.is_always_loaded_algorithm("LUFactorization") == true + @test LinearSolveAutotune.is_always_loaded_algorithm("GenericLUFactorization") == true + @test LinearSolveAutotune.is_always_loaded_algorithm("MKLLUFactorization") == true + @test LinearSolveAutotune.is_always_loaded_algorithm("AppleAccelerateLUFactorization") == true + @test LinearSolveAutotune.is_always_loaded_algorithm("SimpleLUFactorization") == true + + # Test extension-dependent algorithms + @test LinearSolveAutotune.is_always_loaded_algorithm("RFLUFactorization") == false + @test LinearSolveAutotune.is_always_loaded_algorithm("FastLUFactorization") == false + @test LinearSolveAutotune.is_always_loaded_algorithm("BLISLUFactorization") == false + @test LinearSolveAutotune.is_always_loaded_algorithm("CudaOffloadLUFactorization") == false + @test LinearSolveAutotune.is_always_loaded_algorithm("MetalLUFactorization") == false + + # Test unknown algorithm + @test LinearSolveAutotune.is_always_loaded_algorithm("UnknownAlgorithm") == false + end + + @testset "Best Always-Loaded Algorithm Finding" begin + # Create mock benchmark data with both always-loaded and extension-dependent algorithms + mock_data = [ + (size = 150, algorithm = "RFLUFactorization", eltype = "Float64", gflops = 50.0, success = true, error = ""), + (size = 150, algorithm = "LUFactorization", eltype = "Float64", gflops = 30.0, success = true, error = ""), + (size = 150, algorithm = "MKLLUFactorization", eltype = "Float64", gflops = 40.0, success = true, error = ""), + (size = 150, algorithm = "GenericLUFactorization", eltype = "Float64", gflops = 20.0, success = true, error = ""), + # Add Float32 data + (size = 150, algorithm = "LUFactorization", eltype = "Float32", gflops = 25.0, success = true, error = ""), + (size = 150, algorithm = "MKLLUFactorization", eltype = "Float32", gflops = 35.0, success = true, error = ""), + (size = 150, algorithm = "GenericLUFactorization", eltype = "Float32", gflops = 15.0, success = true, error = ""), + ] + test_df = DataFrame(mock_data) + + # Test finding best always-loaded algorithm for Float64 medium size + best_always_loaded = LinearSolveAutotune.find_best_always_loaded_algorithm( + test_df, "Float64", "medium (100-300)") + @test best_always_loaded == "MKLLUFactorization" # Best among always-loaded (40.0 > 30.0 > 20.0) + + # Test finding best always-loaded algorithm for Float32 medium size + best_always_loaded_f32 = LinearSolveAutotune.find_best_always_loaded_algorithm( + test_df, "Float32", "medium (100-300)") + @test best_always_loaded_f32 == "MKLLUFactorization" # Best among always-loaded (35.0 > 25.0 > 15.0) + + # Test with no data for a size range + no_result = LinearSolveAutotune.find_best_always_loaded_algorithm( + test_df, "Float64", "large (300-1000)") + @test no_result === nothing + + # Test with unknown element type + no_result_et = LinearSolveAutotune.find_best_always_loaded_algorithm( + test_df, "ComplexF64", "medium (100-300)") + @test no_result_et === nothing + end + + @testset "Dual Preference System" begin # Clear any existing preferences first LinearSolveAutotune.clear_algorithm_preferences() - # Set test preferences - LinearSolveAutotune.set_algorithm_preferences(test_categories) + # Create mock benchmark data + mock_data = [ + (size = 150, algorithm = "RFLUFactorization", eltype = "Float64", gflops = 50.0, success = true, error = ""), + (size = 150, algorithm = "LUFactorization", eltype = "Float64", gflops = 30.0, success = true, error = ""), + (size = 150, algorithm = "MKLLUFactorization", eltype = "Float64", gflops = 40.0, success = true, error = ""), + (size = 150, algorithm = "GenericLUFactorization", eltype = "Float64", gflops = 20.0, success = true, error = ""), + # Add Float32 data where MKL is best overall + (size = 150, algorithm = "LUFactorization", eltype = "Float32", gflops = 25.0, success = true, error = ""), + (size = 150, algorithm = "MKLLUFactorization", eltype = "Float32", gflops = 45.0, success = true, error = ""), + (size = 150, algorithm = "GenericLUFactorization", eltype = "Float32", gflops = 15.0, success = true, error = ""), + ] + + test_df = DataFrame(mock_data) + + # Test categories: RFLU best for Float64, MKL best for Float32 + test_categories = Dict{String, String}( + "Float64_medium (100-300)" => "RFLUFactorization", + "Float32_medium (100-300)" => "MKLLUFactorization" + ) + + # Set preferences with benchmark data for intelligent fallback selection + LinearSolveAutotune.set_algorithm_preferences(test_categories, test_df) # Get preferences back retrieved_prefs = LinearSolveAutotune.get_algorithm_preferences() - @test isa(retrieved_prefs, Dict{String, String}) + @test isa(retrieved_prefs, Dict{String, Any}) @test !isempty(retrieved_prefs) - # The new preference system uses different keys (eltype_sizecategory) - # so we just check that preferences were set - @test length(retrieved_prefs) > 0 - - # Test clearing preferences + # Test Float64 preferences + @test haskey(retrieved_prefs, "Float64_medium") + float64_prefs = retrieved_prefs["Float64_medium"] + @test isa(float64_prefs, Dict) + @test haskey(float64_prefs, "best") + @test haskey(float64_prefs, "always_loaded") + @test float64_prefs["best"] == "RFLUFactorization" # Best overall + @test float64_prefs["always_loaded"] == "MKLLUFactorization" # Best always-loaded + + # Test Float32 preferences + @test haskey(retrieved_prefs, "Float32_medium") + float32_prefs = retrieved_prefs["Float32_medium"] + @test isa(float32_prefs, Dict) + @test haskey(float32_prefs, "best") + @test haskey(float32_prefs, "always_loaded") + @test float32_prefs["best"] == "MKLLUFactorization" # Best overall + @test float32_prefs["always_loaded"] == "MKLLUFactorization" # Same as best (already always-loaded) + + # Test that both preference types are actually set in LinearSolve + using Preferences + @test Preferences.has_preference(LinearSolve, "best_algorithm_Float64_medium") + @test Preferences.has_preference(LinearSolve, "best_always_loaded_Float64_medium") + @test Preferences.has_preference(LinearSolve, "best_algorithm_Float32_medium") + @test Preferences.has_preference(LinearSolve, "best_always_loaded_Float32_medium") + + # Verify the actual preference values + @test Preferences.load_preference(LinearSolve, "best_algorithm_Float64_medium") == "RFLUFactorization" + @test Preferences.load_preference(LinearSolve, "best_always_loaded_Float64_medium") == "MKLLUFactorization" + @test Preferences.load_preference(LinearSolve, "best_algorithm_Float32_medium") == "MKLLUFactorization" + @test Preferences.load_preference(LinearSolve, "best_always_loaded_Float32_medium") == "MKLLUFactorization" + + # Test clearing dual preferences LinearSolveAutotune.clear_algorithm_preferences() cleared_prefs = LinearSolveAutotune.get_algorithm_preferences() @test isempty(cleared_prefs) + + # Verify preferences are actually cleared from LinearSolve + @test !Preferences.has_preference(LinearSolve, "best_algorithm_Float64_medium") + @test !Preferences.has_preference(LinearSolve, "best_always_loaded_Float64_medium") + @test !Preferences.has_preference(LinearSolve, "best_algorithm_Float32_medium") + @test !Preferences.has_preference(LinearSolve, "best_always_loaded_Float32_medium") + end + + @testset "Dual Preference Fallback Logic" begin + # Test fallback logic when no benchmark data is provided + LinearSolveAutotune.clear_algorithm_preferences() + + # Test categories with extension-dependent algorithms but no benchmark data + test_categories_no_data = Dict{String, String}( + "Float64_medium (100-300)" => "RFLUFactorization", + "ComplexF64_medium (100-300)" => "RFLUFactorization" + ) + + # Set preferences WITHOUT benchmark data (should use fallback logic) + LinearSolveAutotune.set_algorithm_preferences(test_categories_no_data, nothing) + + # Get preferences back + retrieved_prefs = LinearSolveAutotune.get_algorithm_preferences() + + # Test Float64 fallback logic + @test haskey(retrieved_prefs, "Float64_medium") + float64_prefs = retrieved_prefs["Float64_medium"] + @test float64_prefs["best"] == "RFLUFactorization" + # Should fall back to LUFactorization for real types when no MKL detected + @test float64_prefs["always_loaded"] == "LUFactorization" + + # Test ComplexF64 fallback logic + @test haskey(retrieved_prefs, "ComplexF64_medium") + complex_prefs = retrieved_prefs["ComplexF64_medium"] + @test complex_prefs["best"] == "RFLUFactorization" + # Should fall back to LUFactorization for complex types (conservative) + @test complex_prefs["always_loaded"] == "LUFactorization" + + # Clean up + LinearSolveAutotune.clear_algorithm_preferences() + end + + @testset "Integration: Dual Preferences Set in autotune_setup" begin + # Test that autotune_setup actually sets dual preferences + LinearSolveAutotune.clear_algorithm_preferences() + + # Run a minimal autotune that sets preferences + result = LinearSolveAutotune.autotune_setup( + sizes = [:tiny], + set_preferences = true, # KEY: Must be true to test preference setting + samples = 1, + seconds = 0.1, + eltypes = (Float64,) + ) + + @test isa(result, AutotuneResults) + + # Check if any preferences were set + prefs_after_autotune = LinearSolveAutotune.get_algorithm_preferences() + + # If autotune found and categorized results, we should have dual preferences + if !isempty(prefs_after_autotune) + # Pick the first preference set to test + first_key = first(keys(prefs_after_autotune)) + first_prefs = prefs_after_autotune[first_key] + + @test isa(first_prefs, Dict) + @test haskey(first_prefs, "best") + @test haskey(first_prefs, "always_loaded") + @test first_prefs["best"] !== nothing + @test first_prefs["always_loaded"] !== nothing + + # Both should be valid algorithm names + @test isa(first_prefs["best"], String) + @test isa(first_prefs["always_loaded"], String) + @test !isempty(first_prefs["best"]) + @test !isempty(first_prefs["always_loaded"]) + + # The always_loaded algorithm should indeed be always loaded + @test LinearSolveAutotune.is_always_loaded_algorithm(first_prefs["always_loaded"]) + end + + # Clean up + LinearSolveAutotune.clear_algorithm_preferences() end @testset "AutotuneResults Type" begin