diff --git a/benchmark/__plot_results.jl b/benchmark/__plot_results.jl new file mode 100644 index 000000000..fd1ed646a --- /dev/null +++ b/benchmark/__plot_results.jl @@ -0,0 +1,301 @@ +#!/usr/bin/env julia +# Weak-scaling plots (1/2/4/8 GPUs) for the benchmark result CSVs. +# One figure per benchmark, three panels: throughput, time/step, parallel efficiency. +# +# CSV schema (see src/core.jl save_result): +# implementation,gpus,N,M,trial,time_ms,throughput,correctness +# `throughput` is the benchmark's `total_flops` divided by elapsed time. For +# Gray-Scott that unit is Gpoint-updates/s; other benchmarks report GFLOP/s. +# +# save_result appends and does NOT encode the code-path variant, so a cuNumeric CSV +# holds alternating runs: baseline, @accelerate, baseline, @accelerate, ... +# (a run boundary = the GPU count resetting downward). +# cuPyNumeric / CUDA.jl have no accelerated path -> a single block. +# +# Encoding: color = implementation; line style = code path +# solid = baseline, dashed = @accelerate. + +using Plots +using Statistics + +gr() + +function parse_args(args) + results_dir = "results" + out_dir = nothing + single_cunumeric_run = :baseline + hide_baseline = false + output_suffix = "" + + for arg in args + if startswith(arg, "--single-cu=") + value = Symbol(lowercase(last(split(arg, "="; limit=2)))) + value in (:baseline, :accelerated) || + error("--single-cu must be baseline or accelerated") + single_cunumeric_run = value + elseif arg == "--hide-baseline" + hide_baseline = true + elseif startswith(arg, "--out=") + out_dir = last(split(arg, "="; limit=2)) + elseif startswith(arg, "--suffix=") + output_suffix = last(split(arg, "="; limit=2)) + else + results_dir = arg + end + end + isempty(output_suffix) && hide_baseline && (output_suffix = "_no_baseline") + + results_dir = isabspath(results_dir) ? results_dir : joinpath(@__DIR__, results_dir) + if out_dir === nothing + out_dir = if basename(normpath(results_dir)) == "results" + joinpath(@__DIR__, "plots") + else + joinpath(@__DIR__, "plots", basename(normpath(results_dir))) + end + else + out_dir = isabspath(out_dir) ? out_dir : joinpath(@__DIR__, out_dir) + end + return (; results_dir, out_dir, single_cunumeric_run, hide_baseline, output_suffix) +end + +const CONFIG = parse_args(ARGS) +const RESULTS_DIR = CONFIG.results_dir +const OUT_DIR = CONFIG.out_dir +const SINGLE_CUNUMERIC_RUN = CONFIG.single_cunumeric_run +const HIDE_BASELINE = CONFIG.hide_baseline +const OUTPUT_SUFFIX = CONFIG.output_suffix + +# filekey, family label, color, marker, can_contain_accelerated_blocks +const FAMILIES = [ + ("cunumeric", "cuNumeric.jl (fused)", "#2a78d6", :circle, true), + ("cunumeric_nofusion", "cuNumeric.jl (unfused)", "#4a3aa7", :diamond, true), + ("cupynumeric", "cuPyNumeric", "#eb6834", :rect, false), + ("CUDA.jl", "CUDA.jl", "#008300", :utriangle, false), +] + +const INK = "#0b0b0b" +const MUTED = "#898781" +const GRIDCOL = "#e1e0d9" +const IDEALCOL = "#c3c2b7" + +struct Row + gpus::Int + time_ms::Float64 + thr::Float64 +end + +# Parse a CSV into runs, split wherever the GPU count resets to a smaller value. +function load_runs(path) + rows = Row[] + for line in eachline(path) + isempty(strip(line)) && continue + f = split(line, ',') + push!(rows, Row(parse(Int, f[2]), parse(Float64, f[6]), parse(Float64, f[7]))) + end + isempty(rows) && return Vector{Row}[] + runs = [Row[]] + for (i, r) in enumerate(rows) + i > 1 && r.gpus < rows[i - 1].gpus && push!(runs, Row[]) + push!(runs[end], r) + end + return runs +end + +# Aggregate trials per GPU count -> sorted vector of (gpus, t, tsd, h, hsd). +function aggregate(rows) + by = Dict{Int,Vector{Row}}() + for r in rows + push!(get!(by, r.gpus, Row[]), r) + end + sd(x) = length(x) > 1 ? std(x) : 0.0 + return [ + (gpus=g, t=mean(getfield.(by[g], :time_ms)), tsd=sd(getfield.(by[g], :time_ms)), + h=mean(getfield.(by[g], :thr)), hsd=sd(getfield.(by[g], :thr))) + for g in sort(collect(keys(by))) + ] +end + +# Build the series (color+marker+linestyle+agg) present for one benchmark. +function series_for(bench) + series = [] # NamedTuple(label,color,marker,ls,agg) + for (key, fam, color, marker, splits) in FAMILIES + path = joinpath(RESULTS_DIR, "$(bench)_$(key).csv") + isfile(path) || continue + runs = load_runs(path) + isempty(runs) && continue + if splits && bench == "grayscott" + if length(runs) == 1 + label = + SINGLE_CUNUMERIC_RUN === :accelerated ? "$fam · accelerated" : + "$fam · baseline" + ls = SINGLE_CUNUMERIC_RUN === :accelerated ? :dash : :solid + push!( + series, (label=label, color=color, marker=marker, ls=ls, + agg=aggregate(runs[1])) + ) + else + # Repeated harness runs append alternating baseline/accelerated blocks. + baseline_rows = reduce(vcat, runs[1:2:end]) + accelerated_rows = reduce(vcat, runs[2:2:end]) + push!( + series, + (label="$fam · baseline", color=color, marker=marker, + ls=:solid, agg=aggregate(baseline_rows)), + ) + push!(series, + (label="$fam · accelerated", color=color, marker=marker, + ls=:dash, agg=aggregate(accelerated_rows))) + end + else + push!( + series, + (label=fam, color=color, marker=marker, ls=:solid, + agg=aggregate(reduce(vcat, runs))), + ) + end + end + return series +end + +function throughput_label(bench) + return bench == "grayscott" ? + "Throughput (Gpoint-updates/s)" : "Throughput (GFLOP/s)" +end + +function addline!(p, s, y; kw...) + return plot!(p, getfield.(s.agg, :gpus), y; color=s.color, + lw=2.2, ls=s.ls, marker=s.marker, ms=6, msc=s.color, markerstrokewidth=0.8, + label=s.label, kw...) +end + +# One legend key: a short line sample (+ optional marker) with a text label. +function swatch!(p, x, y, color, ls, marker, label) + plot!(p, [x, x + 0.032], [y, y]; color=color, lw=2.6, ls=ls, label="") + marker !== nothing && scatter!(p, [x + 0.016], [y]; color=color, marker=marker, + ms=6, msc=color, markerstrokewidth=0.8, label="") + return annotate!(p, x + 0.045, y, text(label, 9, INK, :left)) +end + +# Grouped legend: color/marker = implementation, line style = code path. +function build_legend(series) + pl = plot(; framestyle=:none, legend=false, xlims=(0, 1), ylims=(0, 1), + grid=false, ticks=false) + # implementations present, in FAMILIES order, matched by color + present = [ + (fam, color, marker) for (key, fam, color, marker, _) in FAMILIES + if any(s.color == color for s in series) + ] + annotate!(pl, 0.015, 0.74, text("Implementation", 10, INK, :left)) + xs = range(0.18, 0.80; length=max(length(present), 1)) + for ((fam, color, marker), x) in zip(present, xs) + swatch!(pl, x, 0.74, color, :solid, marker, fam) + end + annotate!(pl, 0.015, 0.26, text("Line style", 10, INK, :left)) + has_baseline = any(endswith(s.label, "· baseline") for s in series) + has_accelerated = any(endswith(s.label, "· accelerated") for s in series) + if has_baseline && has_accelerated + swatch!(pl, 0.18, 0.26, MUTED, :solid, nothing, "baseline") + swatch!(pl, 0.40, 0.26, MUTED, :dash, nothing, "@accelerate") + swatch!(pl, 0.70, 0.26, IDEALCOL, :dashdot, nothing, "efficiency = 1") + elseif has_accelerated + swatch!(pl, 0.18, 0.26, MUTED, :dash, nothing, "@accelerate") + swatch!(pl, 0.52, 0.26, IDEALCOL, :dashdot, nothing, "efficiency = 1") + elseif has_baseline + swatch!(pl, 0.18, 0.26, MUTED, :solid, nothing, "baseline") + swatch!(pl, 0.48, 0.26, IDEALCOL, :dashdot, nothing, "efficiency = 1") + else + swatch!(pl, 0.18, 0.26, IDEALCOL, :dashdot, nothing, "efficiency = 1") + end + return pl +end + +function positive_ylim(vals; pad=0.12) + isempty(vals) && return (0, 1) + hi = maximum(vals) + hi > 0 || return (0, 1) + return (0, hi * (1 + pad)) +end + +function main() + mkpath(OUT_DIR) + files = filter(f -> endswith(f, ".csv"), readdir(RESULTS_DIR)) + benches = unique( + String[ + m.captures[1] for f in files for (key, _, _, _, _) in FAMILIES + for m in (match(Regex("^(.*)_" * replace(key, "." => "\\.") * "\\.csv\$"), f),) + if m !== nothing + ], + ) + + for bench in benches + series = series_for(bench) + if HIDE_BASELINE + series = filter(s -> !endswith(s.label, "· baseline"), series) + end + isempty(series) && continue + + common = (xscale=:log2, xticks=([1, 2, 4, 8], ["1", "2", "4", "8"]), xlabel="GPUs", + framestyle=:box, grid=true, gridcolor=GRIDCOL, gridalpha=1.0, + foreground_color_text=INK, tickfontcolor=MUTED, legend=false, + xlims=(0.85, 9.4)) + + # Panel 1: throughput (higher better) + throughput = [x.h for s in series for x in s.agg] + p1 = plot(; ylabel=throughput_label(bench), title="Throughput", + ylims=positive_ylim(throughput), common...) + for s in series + addline!(p1, s, getfield.(s.agg, :h); yerror=getfield.(s.agg, :hsd)) + end + + # Panel 2: time per step (lower better; ideal = flat) + p2 = plot(; ylabel="Time / step (ms)", title="Time per step", common...) + for s in series + addline!(p2, s, getfield.(s.agg, :t); yerror=getfield.(s.agg, :tsd)) + end + + # Panel 3: parallel efficiency = thr(p)/(p*thr(1)); ideal = 1.0 + efficiencies = Float64[] + for s in series + i1 = findfirst(x -> x.gpus == 1, s.agg) + i1 === nothing && continue + base = s.agg[i1].h + append!(efficiencies, [x.h/(x.gpus*base) for x in s.agg]) + end + p3 = plot(; ylabel="Parallel efficiency", title="Weak-scaling efficiency", + ylims=positive_ylim(vcat(efficiencies, [1.0])), common...) + hline!(p3, [1.0]; color=IDEALCOL, ls=:dashdot, lw=1.4, label="") + for s in series + i1 = findfirst(x -> x.gpus == 1, s.agg) + i1 === nothing && continue + base = s.agg[i1].h + addline!(p3, s, [x.h/(x.gpus*base) for x in s.agg]) + end + + # grouped legend panel: color/marker = implementation, style = code path + pl = build_legend(series) + has_baseline = any(endswith(s.label, "· baseline") for s in series) + has_accelerated = any(endswith(s.label, "· accelerated") for s in series) + style_title = if has_baseline && has_accelerated + "solid = baseline · dashed = @accelerate" + elseif has_accelerated + "dashed = @accelerate" + elseif has_baseline + "solid = baseline" + else + "implementation comparison" + end + + fig = plot(p1, p2, p3, pl; layout=@layout([grid(1, 3); leg{0.16h}]), + size=(1400, 600), dpi=200, + plot_title=titlecase(bench) * " — weak scaling ($style_title)", + plot_titlefontsize=12, left_margin=6Plots.mm, + bottom_margin=6Plots.mm, top_margin=4Plots.mm, + background_color="#fcfcfb") + + out = joinpath(OUT_DIR, "$(bench)_weak_scaling$(OUTPUT_SUFFIX).png") + savefig(fig, out) + println("wrote $out") + end +end + +main() diff --git a/docs/src/api_binary.md b/docs/src/api_binary.md index 7285ae15e..ae6d72e82 100644 --- a/docs/src/api_binary.md +++ b/docs/src/api_binary.md @@ -6,7 +6,13 @@ The following binary operations are supported and can be applied elementwise to pairs of `NDArray` values: -- `+`, `-`, `*`, `/`, `^`, `<`, `<=`, `>`, `>=`, `==`, `!=`, `atan`, `hypot`, `max`, `min`, `lcm`, `gcd` +- `+`, `-`, `*`, `/`, `^`, `%`, `<`, `<=`, `>`, `>=`, `==`, `!=`, `&`, `|`, `⊻`, `<<`, `>>`, `atan`, `hypot`, `max`, `min`, `lcm`, `gcd`, `fld`, `mod`, `rem`, `copysign` + +## Differences from Base Julia + +- `div` / `÷` are not provided. cuPyNumeric floor-divide matches Julia `fld` (toward `-Inf`), not truncated `div`. For example `-7 ÷ 2` is `-3` in Julia and `-4` for `fld`. +- `&`, `|`, `⊻` are integer and `Bool` bitwise ops. `<<` and `>>` are integers excluding `Bool`. +- `copysign` is float-only. ```@autodocs Modules = [cuNumeric] diff --git a/docs/src/api_unary.md b/docs/src/api_unary.md index e2099a911..da783bccf 100644 --- a/docs/src/api_unary.md +++ b/docs/src/api_unary.md @@ -6,11 +6,14 @@ The following unary operations are supported and can be broadcast over `NDArray`: -- `-`, `!`, `abs`, `acos`, `acosh`, `asin`, `asinh`, `atan`, `atanh`, `cbrt`, `conj`, `cos`, `cosh`, `deg2rad`, `exp`, `exp2`, `expm1`, `floor`, `imag`, `isfinite`, `log`, `log10`, `log1p`, `log2`, `rad2deg`, `real`, `sign`, `signbit`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`, `^2`, `^-1` or `inv` +- `-`, `!`, `~`, `abs`, `acos`, `acosh`, `asin`, `asinh`, `atan`, `atanh`, `cbrt`, `ceil`, `conj`, `cos`, `cosh`, `deg2rad`, `exp`, `exp2`, `expm1`, `floor`, `imag`, `isfinite`, `isinf`, `isnan`, `log`, `log10`, `log1p`, `log2`, `rad2deg`, `real`, `round`, `sign`, `signbit`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`, `trunc`, `^2`, `^-1` or `inv` ## Differences from Base Julia - The `acosh` function in Julia will error on inputs outside of the domain (`x >= 1`), but cuNumeric.jl will return `NaN`. +- `round.(A)` uses IEEE round-to-nearest-even (the `RINT` kernel), matching Julia `round(x)` / `RoundNearest`. Only that 1-arg path is wired. `digits`, `sigdigits`, and `RoundingMode` are not supported (`round.(A; digits=n)` errors). +- `floor`, `ceil`, `trunc`, and `signbit` are float-only kernels. `round` also supports complex values. Bool and integer inputs are not accepted (Julia's `floor`/`ceil`/`trunc`/`round` on integers are identity). +- `~` is bitwise not on integers. On `Bool` it matches `!` (the invert kernel rejects `Bool`, so we use logical not). ```@autodocs Modules = [cuNumeric] diff --git a/src/ndarray/binary.jl b/src/ndarray/binary.jl index 07745f483..4825af20f 100644 --- a/src/ndarray/binary.jl +++ b/src/ndarray/binary.jl @@ -1,11 +1,11 @@ # Still missing: -# # Base.copysign => cuNumeric.COPYSIGN, #* ANNOYING TO TEST -# #missing => cuNumeric.fmod, #same as mod in Julia? -# # Base.isapprox => cuNumeric.ISCLOSE, #* HANDLE rtol, atol kwargs!!! -# # Base.ldexp => cuNumeric.LDEXP, #* LHS FLOATS, RHS INTS -# #missing => cuNumeric.LOGADDEXP, -# #missing => cuNumeric.LOGADDEXP2, -# #missing => cuNumeric.NEXTAFTER, +# # Base.isapprox => cuNumeric.ISCLOSE, # rtol, atol kwargs +# # Base.ldexp => cuNumeric.LDEXP, # LHS floats, RHS ints +# # missing => cuNumeric.LOGADDEXP, +# # missing => cuNumeric.LOGADDEXP2, +# # missing => cuNumeric.NEXTAFTER, +# # Base.div / ÷ — FLOOR_DIVIDE matches Julia `fld` (toward -Inf), not +# # truncated `div`. Do not ship as `div`: `-7 ÷ 2` is -3 in Julia, -4 for fld. # Binary ops which are equivalent to Julia's broadcast syntax global const binary_op_map = Dict{Function,BinaryOpCode}( @@ -24,14 +24,18 @@ global const binary_op_map = Dict{Function,BinaryOpCode}( Base.:(==) => cuNumeric.EQUAL, #* BE SURE TO DEFINE NON-BROADCASTED VERSION (BINARY_REDUCTION), Base.lcm => cuNumeric.LCM, Base.gcd => cuNumeric.GCD, - # Base.xor => cuNumeric.LOGICAL_XOR, #! DO LATER - # Base.:⊻ => cuNumeric.LOGICAL_XOR, #! DO LATER - # Base.div => cuNumeric.FLOOR_DIVIDE, #! THESE ARE IN-EXACT FOR INTS? - # Base.:(÷) => cuNumeric.FLOOR_DIVIDE, #! THESE ARE IN-EXACT FOR INTS? - # Base.:(>>) => cuNumeric.RIGHT_SHIFT, #! DO LATER - # Base.:(<<) => cuNumeric.LEFT_SHIFT, #! DO LATER - # Base.:(&&) => (cuNumeric.LOGICAL_AND, Bool, :same_as_input), #! CANNOT OVERLOAD WTF? (see Base.andand) - # Base.:(||) => (cuNumeric.LOGICAL_OR, Bool, :same_as_input), #! CANNOT OVERLOAD WTF? + Base.:(&) => cuNumeric.BITWISE_AND, # integers and Bool + Base.:(|) => cuNumeric.BITWISE_OR, + Base.:(⊻) => cuNumeric.BITWISE_XOR, + Base.:(<<) => cuNumeric.LEFT_SHIFT, # integers, not Bool + Base.:(>>) => cuNumeric.RIGHT_SHIFT, # integers, not Bool + Base.fld => cuNumeric.FLOOR_DIVIDE, # matches Julia fld, not div/÷ + Base.mod => cuNumeric.MOD, + Base.rem => cuNumeric.FMOD, + Base.:(%) => cuNumeric.FMOD, # Julia `%` is rem + Base.copysign => cuNumeric.COPYSIGN, # floats only + # Base.:(&&) => (cuNumeric.LOGICAL_AND, Bool, :same_as_input), # cannot overload (see Base.andand) + # Base.:(||) => (cuNumeric.LOGICAL_OR, Bool, :same_as_input), # cannot overload ) global const floaty_binary_op_map = Dict{Function,BinaryOpCode}( @@ -156,14 +160,14 @@ function Base.:(*)(rhs1::NDArray{A,2}, rhs2::NDArray{B,2}) where {A,B} end function Base.:(*)(rhs1::NDArray{Bool,2}, rhs2::NDArray{Bool,2}) - throw( + return throw( ArgumentError("cuNumeric.jl does not support matrix multiplication of two Boolean arrays") ) end function Base.:(*)(rhs1::NDArray{<:Integer,2}, rhs2::NDArray{<:Integer,2}) #* this is a stupid..... - throw( + return throw( ArgumentError("cuNumeric.jl does not support matrix multiplication of two Integer arrays") ) end @@ -221,14 +225,14 @@ end function LinearAlgebra.mul!(out::NDArray, rhs1::NDArray{Bool,2}, rhs2::NDArray{Bool,2}) #* Could just promote both inputs to Int32 - throw( + return throw( ArgumentError("cuNumeric.jl does not support matrix multiplication of two Boolean arrays") ) end function LinearAlgebra.mul!(out::NDArray, rhs1::NDArray{<:Integer,2}, rhs2::NDArray{<:Integer,2}) #* this is a stupid..... - throw( + return throw( ArgumentError("cuNumeric.jl does not support matrix multiplication of two Integer arrays") ) end diff --git a/src/ndarray/unary.jl b/src/ndarray/unary.jl index 0c0df9cac..c4fe267d5 100644 --- a/src/ndarray/unary.jl +++ b/src/ndarray/unary.jl @@ -26,19 +26,24 @@ global const floaty_unary_ops_no_args = Dict{Function,UnaryOpCode}( global const unary_op_map_no_args = Dict{Function,UnaryOpCode}( Base.abs => cuNumeric.ABSOLUTE, - # Base.conj => cuNumeric.CONJ, #! NEED TO SUPPORT COMPLEX TYPES FIRST + # Base.conj => cuNumeric.CONJ, # handled as a special case below Base.:(-) => cuNumeric.NEGATIVE, - # Base.frexp => cuNumeric.FREXP, #* annoying returns tuple - # missing => cuNumeric.GETARG, #not in numpy? - # Base.imag => cuNumeric.IMAG, #! NEED TO SUPPORT COMPLEX TYPES FIRST - # missing => cuNumerit.INVERT, # no bitwise not in julia? - # Base.isfinite => cuNumeric.ISFINITE, #* dont feel like looking into Inf rn - # Base.isinf => cuNumeric.ISINF, #* dont feel like looking into Inf rn - # Base.isnan => cuNumeric.ISNAN, #* dont feel like looking into Inf rn - # Base.modf => cuNumeric.MODF, #* annoying returns tuple - #missing => cuNumeric.POSITIVE, #What is this even for + # Base.frexp => cuNumeric.FREXP, # returns a tuple + # missing => cuNumeric.GETARG, + # Base.imag => cuNumeric.IMAG, # handled as a special case below + Base.:(~) => cuNumeric.INVERT, # integers only; kernel rejects Bool + Base.isfinite => cuNumeric.ISFINITE, + Base.isinf => cuNumeric.ISINF, + Base.isnan => cuNumeric.ISNAN, + # Base.modf => cuNumeric.MODF, # returns a tuple + # missing => cuNumeric.POSITIVE, Base.sign => cuNumeric.SIGN, - # Base.signbit => cuNumeric.SIGNBIT, #! Doesnt support Bool, I do not feel like dealing with this right now... + Base.signbit => cuNumeric.SIGNBIT, # floats only; kernel rejects Bool/int + Base.ceil => cuNumeric.CEIL, # floats only + Base.floor => cuNumeric.FLOOR, # floats only + Base.trunc => cuNumeric.TRUNC, # floats only + # 1-arg `round.(A)` only (see __broadcast below). ROUND needs extra_args. + Base.round => cuNumeric.RINT, ) ### SPECIAL CASES ### @@ -105,6 +110,17 @@ function Base.:(-)(input::NDArray{Bool}) return out end +# Broadcast `.-` on Bool: Julia `-true === -1`, so promote then NEGATIVE. +@inline function __broadcast( + ::typeof(Base.:(-)), out::NDArray{O}, input::NDArray{Bool} +) where {O<:Integer} + assertpromotion(".-", Bool, O) + promoted = unchecked_promote_arr(input, O) + result = nda_unary_op!(out, cuNumeric.NEGATIVE, promoted) + destroy!(promoted) + return result +end + function Base.sqrt(input::NDArray{T,2}) where {T} return error("cuNumeric.jl does not support matrix square root.") end @@ -169,6 +185,32 @@ for (julia_fn, op_code) in unary_op_map_no_args end end +# INVERT rejects Bool; on Bool, Julia `~` is the same as `!`. +@inline function __broadcast(::typeof(Base.:(~)), out::NDArray{Bool}, input::NDArray{Bool}) + return nda_unary_op!(out, cuNumeric.LOGICAL_NOT, input) +end + +@noinline function _unsupported_round_broadcast() + return throw( + ArgumentError( + "cuNumeric.jl only supports round.(A) (default RoundNearest / IEEE rint). " * + "digits, sigdigits, and RoundingMode are not supported.", + ), + ) +end + +# Reject keyword forms before fusion captures Julia's keyword wrapper as a GPU callable. +@inline function Base.Broadcast.broadcasted_kwsyntax( + ::typeof(Base.round), ::NDArray; kwargs... +) + return _unsupported_round_broadcast() +end + +# Only the 1-arg `round.(A)` method above is supported (IEEE rint / RoundNearest). +@inline function __broadcast(::typeof(Base.round), ::NDArray, ::NDArray, extra...) + return _unsupported_round_broadcast() +end + # Some functions always return floats even when given integers # in the case where the output is determined to be float, but # the input is integer, we first promote the input to float. @@ -192,29 +234,9 @@ for (julia_fn, op_code) in floaty_unary_ops_no_args end end -# global const unary_op_map_with_args = Dict{Function, Int}( -# Base.angle => Int(cuNumeric.ANGLE), -# Base.ceil => Int(cuNumeric.CEIL), #* HAS EXTRA ARGS -# Base.clamp => Int(cuNumeric.CLIP), #* HAS EXTRA ARGS -# Base.floor => cuNumeric.FLOOR, #! Doesnt support Bool, I do not feel like dealing with this right now... -# Base.trunc => Int(cuNumeric.TRUNC) #* HAS EXTRA ARGS -# missing => Int(cuNumeric.RINT), #figure out which version of round -# missing => Int(cuNumeric.ROUND), #figure out which version of round -# ) - -# for (base_func, op_code) in unary_op_map_with_args -# @eval begin -# @doc """ -# $($(Symbol(base_func))) : A unary operation acting on an NDArray -# """ -# function $(Symbol(base_func))(input::NDArray, args...) -# out = cuNumeric.zeros(eltype(input), size(input)) # not sure this is ok for performance -# extra_args = cuNumeric.StdVector{cuNumeric.LegateScalar}([LegateScalar(a) for a in args]) -# unary_op(out, $(op_code), input, extra_args) -# return out -# end -# end -# end +# CLIP / clamp needs extra_args (lo, hi). nda_unary_op! currently ccall's +# without extra scalars, so clamp is not wired. Do not revive the old +# StdVector{LegateScalar} path unless that C API exists. @doc""" Supported Unary Reduction Operations diff --git a/test/array/binary/tests.jl b/test/array/binary/tests.jl index 794c2e19a..de1d2f13a 100644 --- a/test/array/binary/tests.jl +++ b/test/array/binary/tests.jl @@ -36,11 +36,33 @@ function test_binary_operation(func, julia_arr1, julia_arr2, cunumeric_arr1, cun end function test_binary_function_set(func_dict, T, N) - skip = (Base.lcm, Base.gcd) + # Random data includes zeros / huge shift amounts; these are tested separately. + skip = ( + Base.lcm, Base.gcd, Base.fld, Base.mod, Base.rem, Base.:(%), Base.:(<<), Base.:(>>) + ) # not defined for complex. skip_on_complex = ( - Base.:(<), Base.:(<=), Base.:(>), Base.:(>=), Base.max, Base.min, Base.atan, Base.hypot + Base.:(<), + Base.:(<=), + Base.:(>), + Base.:(>=), + Base.max, + Base.min, + Base.atan, + Base.hypot, + Base.:(&), + Base.:(|), + Base.:(⊻), + Base.copysign, + Base.fld, + Base.mod, + Base.rem, + Base.:(%), + Base.:(<<), + Base.:(>>), ) + skip_on_float = (Base.:(&), Base.:(|), Base.:(⊻), Base.:(<<), Base.:(>>)) + skip_on_integer = (Base.copysign,) # kernel is float-only; Bool <: Integer @testset "$func" for func in keys(func_dict) @@ -51,6 +73,14 @@ function test_binary_function_set(func_dict, T, N) continue end + if T <: AbstractFloat && (func in skip_on_float) + continue + end + + if T <: Integer && (func in skip_on_integer) + continue + end + (func in skip) && continue arrs_jl = make_julia_arrays(T, N, :uniform; count=2) @@ -98,6 +128,55 @@ function run_binary_ops_tests(types) end end + # fld/mod/rem/% need a non-zero divisor. FLOOR_DIVIDE is fld, not div. + if (T <: cuNumeric.SUPPORTED_INT_TYPES && T != Bool) || T <: AbstractFloat + if T <: Integer + arr_jl_div = my_rand(T, N; L=(T <: Unsigned ? 1 : -20), R=20) + arr_jl_den = my_rand(T, N; L=1, R=20) + else + arr_jl_div = my_rand(T, N) + arr_jl_den = my_rand(T, N) + arr_jl_den = map( + x -> abs(x) < T(1) ? copysign(one(T), iszero(x) ? one(T) : x) : x, + arr_jl_den, + ) + end + arr_cn_div = @allowscalar NDArray(arr_jl_div) + arr_cn_den = @allowscalar NDArray(arr_jl_den) + + allowscalar() do + for func in (fld, mod, rem, Base.:%) + @test safe_compare( + func.(arr_jl_div, arr_jl_den), func.(arr_cn_div, arr_cn_den), + atol(T), rtol(T), + ) + end + end + end + + # Shifts: small non-negative amounts. Left shift uses a small lhs to + # avoid C++ undefined behavior on signed overflow. + if T <: cuNumeric.SUPPORTED_INT_TYPES && T != Bool + max_shift = T(min(7, 8 * sizeof(T) - 1)) + arr_jl_sh = my_rand(T, N; L=0, R=max_shift) + arr_jl_lshift_lhs = my_rand(T, N; L=0, R=7) + arr_jl_rshift_lhs = my_rand(T, N) + arr_cn_sh = @allowscalar NDArray(arr_jl_sh) + arr_cn_lshift_lhs = @allowscalar NDArray(arr_jl_lshift_lhs) + arr_cn_rshift_lhs = @allowscalar NDArray(arr_jl_rshift_lhs) + + allowscalar() do + @test safe_compare( + arr_jl_lshift_lhs .<< arr_jl_sh, arr_cn_lshift_lhs .<< arr_cn_sh, + atol(T), rtol(T), + ) + @test safe_compare( + arr_jl_rshift_lhs .>> arr_jl_sh, arr_cn_rshift_lhs .>> arr_cn_sh, + atol(T), rtol(T), + ) + end + end + allowscalar() do @test unwrap(arr_cn == arr_cn) @test !unwrap(arr_cn == arr_cn2) diff --git a/test/array/unary/tests.jl b/test/array/unary/tests.jl index 558a2c877..3257c0ab0 100644 --- a/test/array/unary/tests.jl +++ b/test/array/unary/tests.jl @@ -47,14 +47,19 @@ function test_unary_operation(func, julia_arr, cunumeric_arr, T) end end -skip_on_integer = (Base.acosh, Base.atanh, Base.atan, Base.acos, Base.asin) -skip_on_bool = (Base.:(-), skip_on_integer...) +skip_on_integer = ( + Base.acosh, Base.atanh, Base.atan, Base.acos, Base.asin, + Base.ceil, Base.floor, Base.trunc, Base.round, Base.signbit, +) +skip_on_bool = skip_on_integer skip_on_complex = ( Base.tanh, Base.deg2rad, Base.rad2deg, Base.sign, Base.cbrt, Base.exp2, Base.expm1, Base.log10, Base.log1p, Base.log2, Base.acos, Base.asin, Base.atan, Base.acosh, Base.asinh, Base.atanh, + Base.ceil, Base.floor, Base.trunc, Base.signbit, Base.:(~), ) +skip_on_float = (Base.:(~),) function test_unary_function_set(func_dict, T, N) default_generator = (T == Bool) ? :uniform : :unit_interval @@ -73,6 +78,10 @@ function test_unary_function_set(func_dict, T, N) continue end + if func in skip_on_float && (T <: AbstractFloat) + continue + end + domain_type = get(SPECIAL_DOMAINS, func, default_generator) # :uniform is the only generator capable of generating bits @@ -126,6 +135,13 @@ function run_unary_tests(types; include_bool_reductions::Bool=false) allowpromotion(T == Bool) do return test_unary_function_set(cuNumeric.unary_op_map_no_args, T, N) end + + if T <: AbstractFloat + @testset "round is 1-arg only" begin + a = cuNumeric.ones(T, 4) + @test_throws ArgumentError round.(a; digits=1) + end + end # Special cases for unary ops that dont use . syntax @testset "- (Negation)" begin arr = my_rand(T, N)