Skip to content

Commit 3601b68

Browse files
authored
Add Narrow type (#15)
1 parent 8a42545 commit 3601b68

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ BitPacking.bitwidth
1818
```@docs
1919
NArray
2020
NarrowArray
21+
Narrow
2122
```
2223

2324
## Narrow tuples

src/BitPacking.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
module BitPacking
22

3-
import Adapt
43
using Republic
4+
import Adapt
55

66
@public bitwidth
77
export NArray, NVector, NMatrix
88
export NarrowArray, NarrowVector, NarrowMatrix
9+
export Narrow
910
export NarrowTuple, @NarrowTuple
1011
@public Pad, ZeroPad, OnePad
1112

src/NarrowArray.jl

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ function Base.reinterpret(::Type{T}, arr::NarrowArray{S}) where {T,S}
137137
end
138138
end
139139

140-
function Base.Broadcast.broadcasted(::Type{T}, arr::NarrowArray) where T
141-
isbitstype(T) || return Base.Broadcast.Broadcasted(T, (arr,))
140+
function Broadcast.broadcasted(::Type{T}, arr::NarrowArray) where T
141+
isbitstype(T) || return Broadcast.Broadcasted(T, (arr,))
142142

143143
L = length(eltype(parent(arr)))
144144
chunks = SVector{L,T}.(parent(arr))
@@ -147,11 +147,72 @@ end
147147

148148
Base.copy(arr::NarrowArray) = reinterpret(eltype(arr), map(SArray, parent(arr)))
149149

150+
"""
151+
Narrow{T}
152+
153+
Representation tag for the packed form of logical element type `T`. `Narrow` has
154+
no instances; it exists purely for dispatch: passing `Narrow{T}` selects the
155+
packed [`NarrowArray{T}`](@ref) form where plain `T` selects the unpacked form.
156+
157+
| operation | with `T` | with `Narrow{T}` |
158+
|:--------------|:--------------------------------|:--------------------------------|
159+
| `reinterpret` | `reinterpret(T, ::NarrowArray)` | `reinterpret(Narrow{T}, data)` |
160+
| broadcast | `T.(::NarrowArray)` | `Narrow{T}.(array)` |
161+
162+
For broadcast these are value conversions: `T.(narr)` unpacks to dense `T`
163+
values and `Narrow{T}.(array)` packs values into a `NarrowArray{T}`. For
164+
`reinterpret` they are instead bit-preserving views of the same buffer in the two
165+
layouts: `reinterpret(T, narr)` views the packed bits as `T`, while
166+
`reinterpret(Narrow{T}, data)` views an existing array of packed chunks as a
167+
`NarrowArray{T}` without copying.
168+
169+
`Narrow{T}.(array)` makes the narrowing explicit where `NarrowArray{T}(array)`
170+
hides it; the equivalent in-place form is `dest .= expr` for a preallocated
171+
`NarrowArray{T}` destination. All forms use the default chunk length
172+
`pack_count(T)`, so the leading dimension must be a whole number of chunks.
173+
"""
174+
abstract type Narrow{T} end
175+
176+
# Pack `dense` (logical values) into `chunks` by reinterpreting each run of `L`
177+
# values along the first dimension as one `NVector{T,L}`. The fused `.=` writes
178+
# straight into the existing `chunks`, so no packed temporary is allocated.
179+
function _pack_into!(chunks, ::Type{T}, ::Val{L}, dense::AbstractArray{S}) where {T,L,S}
180+
if S === T
181+
chunks .= NVector{T,L}.(reinterpret(NTuple{L,T}, dense))
182+
else
183+
chunks .= NVector{T,L}.(SVector{L,S}.(reinterpret(NTuple{L,S}, dense)))
184+
end
185+
return chunks
186+
end
187+
188+
# `dest .= expr` materializes the (fused) broadcast once, then packs it directly
189+
# into `dest`'s existing parent at `dest`'s own chunk length `L`.
190+
function Base.copyto!(dest::NarrowArray{T,N,L}, bc::Broadcast.Broadcasted{Nothing}) where {T,N,L}
191+
axes(dest) == axes(bc) ||
192+
throw(DimensionMismatch("destination axes $(axes(dest)) do not match broadcast axes $(axes(bc))"))
193+
dense = Broadcast.materialize(Broadcast.broadcasted(bc.f, bc.args...))
194+
_pack_into!(parent(dest), T, Val(L), dense)
195+
return dest
196+
end
197+
198+
Base.similar(arr::NarrowArray) = NarrowArray(similar(parent(arr)))
199+
200+
# `Narrow{T}.(x)` packs the (fused) broadcast `x` into a NarrowArray{T}. Routing
201+
# through the constructor reuses its vectorized, backend-generic packing, so the
202+
# result follows the backend of `x` rather than allocating a host `Array`.
203+
_narrow_broadcast(::Type{T}, x) where T = NarrowArray{T}(Broadcast.materialize(x))
204+
205+
Broadcast.broadcasted(::Type{Narrow{T}}, x) where T = _narrow_broadcast(T, x)
206+
Broadcast.broadcasted(::Type{Narrow{T}}, x::NarrowArray) where T = _narrow_broadcast(T, x)
207+
208+
Base.reinterpret(::Type{Narrow{T}}, arr::AbstractArray) where T =
209+
NarrowArray(reinterpret(narrow_chunk_type(T), arr))
210+
150211
function Base.print_array(io::IO, arr::NarrowArray)
151212
host = Adapt.adapt(Array, arr)
152213
if host isa AbstractVecOrMat
153-
return invoke(Base.print_array, Tuple{IO, AbstractVecOrMat}, io, host)
214+
return @invoke Base.print_array(io::IO, host::AbstractVecOrMat)
154215
else
155-
return invoke(Base.print_array, Tuple{IO, AbstractArray}, io, host)
216+
return @invoke Base.print_array(io::IO, host::AbstractArray)
156217
end
157218
end

test/runtests.jl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,4 +321,64 @@ bits(x) = x
321321
@test_throws ArgumentError reinterpret(UInt16, narrow)
322322
end
323323

324+
@testset "Narrow" begin
325+
values = Bool[1, 0, 1, 0, 1, 0, 1, 0]
326+
327+
# Narrow{T}.(arr) packs into a NarrowArray{T}
328+
packed = Narrow{Bool}.(values)
329+
@test packed isa NarrowVector{Bool}
330+
@test copy(packed) == values
331+
@test packed == NarrowArray{Bool}(values)
332+
333+
# the inner expression fuses, narrowing happens at the boundary
334+
fused = Narrow{Bool}.(.!values)
335+
@test fused isa NarrowVector{Bool}
336+
@test copy(fused) == .!values
337+
338+
# reinterpret(Narrow{T}, bytes) views packed bytes without copying
339+
bytes = UInt8[0x55]
340+
viewed = reinterpret(Narrow{Bool}, bytes)
341+
@test viewed isa NarrowVector{Bool}
342+
@test copy(viewed) == values
343+
bytes[1] = 0x00
344+
@test copy(viewed) == falses(8)
345+
346+
# destination broadcasting packs into preallocated narrow storage
347+
dest = similar(NarrowArray{Bool}(values))
348+
@test dest isa NarrowVector{Bool}
349+
@test size(dest) == size(values)
350+
dest .= values
351+
@test copy(dest) == values
352+
dest .= .!values
353+
@test copy(dest) == .!values
354+
355+
# float4 round trip via both packing forms
356+
f4_values = Float4_E2M1FN[float4(0x01), float4(0x02), float4(0x03), float4(0x04)]
357+
f4_packed = Narrow{Float4_E2M1FN}.(f4_values)
358+
@test f4_packed isa NarrowVector{Float4_E2M1FN}
359+
@test collect(reinterpret(UInt8, f4_packed)) == UInt8[0x21, 0x43]
360+
@test bits.(copy(reinterpret(Narrow{Float4_E2M1FN}, UInt8[0x21, 0x43]))) == bits.(f4_values)
361+
362+
# cross-type packing converts before packing, matching the constructor
363+
@test Narrow{Float4_E2M1FN}.(UInt8[0x01, 0x02, 0x03, 0x04]) == f4_packed
364+
365+
# matrix destination chunks along the first dimension
366+
src = repeat(values, 1, 2)
367+
mat = similar(NarrowArray{Bool}(src))
368+
mat .= src
369+
@test copy(mat) == src
370+
371+
# cross-type destination broadcast converts before packing
372+
f4_dest = similar(f4_packed)
373+
f4_dest .= UInt8[0x01, 0x02, 0x03, 0x04]
374+
@test collect(reinterpret(UInt8, f4_dest)) == UInt8[0x21, 0x43]
375+
376+
# Narrow{T}.(narr) dispatches on a NarrowArray source
377+
@test Narrow{Bool}.(packed) == packed
378+
379+
# print_array handles arrays beyond vectors and matrices
380+
arr3 = NarrowArray{Bool}(reshape(repeat(values, 4), 8, 2, 2))
381+
@test contains(sprint(show, MIME("text/plain"), arr3), "NarrowArray")
382+
end
383+
324384
end

0 commit comments

Comments
 (0)