Skip to content

Commit 642cf8d

Browse files
authored
Merge pull request #3170 from JuliaGPU/tb/int128_abi
Improve Int128 ABI mismatch detection.
1 parent 1caa3ad commit 642cf8d

2 files changed

Lines changed: 89 additions & 19 deletions

File tree

CUDACore/src/compiler/compilation.jl

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,26 @@ function device_layout(@nospecialize(T))
308308
size == sizeof(T) || return :mismatch
309309
return (size, align)
310310
end
311+
# walk `T` and every type reachable from it through type parameters and fields, returning
312+
# `true` as soon as `bad(S)` holds for some reached type `S`. we must look through type
313+
# parameters, not just fields: an aggregate with a mismatching layout is typically reached
314+
# through a pointer (e.g. the element type of a `CuDeviceArray`, carried as a type parameter
315+
# and never as a field), so inspecting only the argument's own fields would miss it and the
316+
# kernel would silently read or write garbage.
317+
function layout_reaches(bad, @nospecialize(T), seen=Base.IdSet{Any}())
318+
(T isa Type && !(T in seen)) || return false
319+
push!(seen, T)
320+
bad(T) && return true
321+
T isa DataType || return false
322+
any(p -> layout_reaches(bad, p, seen), T.parameters) && return true
323+
isconcretetype(T) || return false
324+
any(i -> layout_reaches(bad, fieldtype(T, i), seen), 1:fieldcount(T))
325+
end
326+
311327
device_compatible_layout(@nospecialize(T)) =
312328
# since Julia 1.12, host and device layouts are identical
313-
Base.datatype_alignment(Int128) == 16 || device_layout(T) !== :mismatch
329+
Base.datatype_alignment(Int128) == 16 ||
330+
!layout_reaches(S -> device_layout(S) === :mismatch, T)
314331

315332
# compile to executable machine code
316333
function compile(@nospecialize(job::CompilerJob))
@@ -356,8 +373,7 @@ function compile(@nospecialize(job::CompilerJob))
356373
end
357374
for dt in argtypes
358375
if !device_compatible_layout(dt)
359-
error("""Kernel argument of type $dt contains Int128 fields whose layout differs between this version of Julia and the device.
360-
Use Julia 1.12 or later, where 128-bit integers are aligned to 16 bytes, matching the device.""")
376+
error("Kernel argument of type $dt references 128-bit integer fields. This is only supported on Julia 1.12 or later.")
361377
end
362378
end
363379
param_usage = sum(aligned_sizeof, argtypes)

test/core/execution.jl

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -742,24 +742,78 @@ end
742742
end
743743

744744
@testset "argument layout" begin
745-
kernel(x) = nothing
746-
747-
# plain 128-bit integers occupy their own parameter slot, so are fine
745+
# the back-end aligns 128-bit integers to 16 bytes, but Julia only started doing so in
746+
# 1.12, so aggregates with (U)Int128 fields lay out differently on older hosts. such
747+
# types are rejected there (host==device on 1.12+, so everything is accepted).
748+
@eval struct Int128Wrapper; x::Int64; y::Int128; end
749+
@eval struct FloatWrapper; x::Int64; y::Float64; end # control: no 128-bit integers
750+
host_ok = Base.datatype_alignment(Int128) == 16 # true on Julia 1.12+
751+
752+
# -- the compatibility walk must look through type parameters (e.g. device-array
753+
# element types), not just fields. this part is host-independent. --
754+
reaches_i128(T) = CUDACore.layout_reaches(S -> S === Int128 || S === UInt128, T)
755+
@test reaches_i128(Int128)
756+
@test reaches_i128(Int128Wrapper) # via a field
757+
@test reaches_i128(Tuple{Int64,Int128Wrapper}) # via a tuple element
758+
@test reaches_i128(Ptr{Int128Wrapper}) # via a pointer's pointee
759+
@test reaches_i128(CUDACore.CuDeviceArray{Int128Wrapper,1,1}) # via an element type
760+
@test !reaches_i128(Float64)
761+
@test !reaches_i128(FloatWrapper)
762+
@test !reaches_i128(CUDACore.CuDeviceArray{Float64,1,1})
763+
764+
@test CUDACore.device_layout(Int128) == (16, 16)
765+
@test CUDACore.device_layout(FloatWrapper) == (16, 8)
766+
@test CUDACore.device_layout(Int128Wrapper) === (host_ok ? (32, 16) : :mismatch)
767+
@test CUDACore.device_compatible_layout(Int128Wrapper) == host_ok
768+
@test CUDACore.device_compatible_layout(CUDACore.CuDeviceArray{Int128Wrapper,1,1}) == host_ok
769+
@test CUDACore.device_compatible_layout(CUDACore.CuDeviceArray{Float64,1,1})
770+
771+
# -- end-to-end: rejected on <1.12, compiled and correct on 1.12+ --
772+
773+
# plain 128-bit integers occupy their own parameter slot / array element, and are fine
748774
# regardless of how the host aligns them
749-
@cuda kernel(Int128(1))
750-
@test true
751-
752-
# aggregates with 128-bit fields are only compatible when Julia aligns
753-
# those to 16 bytes like the device does (i.e., on 1.12+)
754-
@eval struct Int128Wrapper
755-
x::Int64
756-
y::Int128
757-
end
758-
if Base.datatype_alignment(Int128) == 16
759-
@cuda kernel(Int128Wrapper(1, 2))
760-
@test true
775+
setval(out, v) = (@inbounds out[1] = v; return)
776+
let out = CuArray{Int128}(undef, 1)
777+
@cuda setval(out, Int128(2)^100 + 7)
778+
@test Array(out)[1] == Int128(2)^100 + 7
779+
end
780+
781+
# aggregate with a 128-bit field, passed as a kernel argument: read its field back so
782+
# the layout check (not an unrelated type error) is what fails on incompatible hosts
783+
gety(out, w) = (@inbounds out[1] = w.y; return)
784+
if host_ok
785+
out = CuArray{Int128}(undef, 1)
786+
@cuda gety(out, Int128Wrapper(42, Int128(2)^100 + 7))
787+
@test Array(out)[1] == Int128(2)^100 + 7
761788
else
762-
@test_throws "layout differs" @cuda kernel(Int128Wrapper(1, 2))
789+
out = CuArray{Int128}(undef, 1)
790+
@test_throws "references 128-bit integer fields" @cuda gety(out, Int128Wrapper(42, Int128(2)^100 + 7))
791+
end
792+
793+
# aggregate with a 128-bit field, reached as a device-array element (memory traffic;
794+
# this is only seen through a pointer, so it used to slip past the argument check)
795+
function readfields(out, A)
796+
i = threadIdx().x
797+
@inbounds begin
798+
out[2i-1] = A[i].x
799+
out[2i] = A[i].y % Int64
800+
end
801+
return
802+
end
803+
wrappers = [Int128Wrapper(1, 2), Int128Wrapper(3, 4)]
804+
if host_ok
805+
dA = CuArray(wrappers); out = CuArray{Int64}(undef, 4)
806+
@cuda threads=2 readfields(out, dA)
807+
@test Array(out) == [1, 2, 3, 4]
808+
else
809+
dA = CuArray(wrappers); out = CuArray{Int64}(undef, 4)
810+
@test_throws "references 128-bit integer fields" @cuda threads=2 readfields(out, dA)
811+
end
812+
813+
# control: an aggregate without 128-bit integers is always accepted
814+
let out = CuArray{Float64}(undef, 1)
815+
@cuda gety(out, FloatWrapper(1, 3.5))
816+
@test Array(out)[1] == 3.5
763817
end
764818
end
765819

0 commit comments

Comments
 (0)