Summary
Thanks for the recent revision. Moving the difference into unbounded integers
and applying abs_int makes the intermediate widths much easier to follow.
One point I could not settle from the current text is that every instruction’s
source operands are qualified as signed or unsigned, whereas the destination is
qualified only by width. This leaves three related questions:
- What does
vabs.v produce for the most negative input,
-2^(SEW-1)—for example, -128 when SEW=8?
- What do
vabd.vv and vabdu.vv produce when the difference reaches
2^SEW - 1—for example, 255 when SEW=8?
- How do
vwabda.vv and vwabdau.vv behave when the accumulator exceeds
the range of a 2*SEW-bit value?
My reading is that the pseudocode already answers (1) and (2), so those cases
may only need a sentence in the normative prose. Case (3) appears to require a
rule of its own. I would be happy to be corrected on any of these points.
(cc @riscv/arc)
Specification Reference
Permalink:
https://github.com/riscv/integer-vector-absolute-difference/blob/60b6370c6/src/unpriv/zvabd.adoc#L68
On dev at commit 60b6370c6:
| Line |
Code |
| L68 |
to_bits(SEW, abs_int(signed(vs2_val[i]))) |
| L112 |
to_bits(SEW, abs_int(signed(vs2_val[i]) - signed(vs1_val[i]))) |
| L161 |
to_bits(SEW, abs_diff) |
| L212 |
to_bits_unsafe(SEW_widen, signed(vd_val[i]) + abs_int(diff)) |
| L264 |
to_bits_unsafe(SEW_widen, unsigned(vd_val[i]) + abs_diff) |
The normative prose I reviewed includes:
norm:vabs_op (L58): “An absolute value operation is performed on each
element of vs2.”
norm:vabd_op (L102) / norm:vabdu_op (L147): the operation is performed
“between the elements of two signed/unsigned integer SEW-bit source operands
vs1 and vs2.”
norm:vwabda_op (L198) / norm:vwabdau_op (L248): the operation
“accumulates the results into the elements of a 2*SEW-bit integer operand
vd.”
The sources are consistently qualified as signed or unsigned, while the
destination is qualified only by width. I may have missed an existing
statement, but searching the document for most negative, saturate,
overflow, wrap, and vxsat returned no hits. I therefore believe that the
signedness of the destination is not stated anywhere.
Expected vs. Observed
| Expected |
Observed |
A rule stating that vabs.v, vabd.vv, and vabdu.vv produce unsigned SEW-bit results |
The destination is qualified only by width. |
A defined result for vabs.v applied to -2^(SEW-1) |
to_bits(8, abs_int(-128)) = to_bits(8, 128) = 0x80; this is 128 when interpreted as unsigned and -128 when interpreted as signed. |
A defined result for vabd.vv at the maximum difference |
to_bits(8, 255) = 0xFF, which represents the correct value only when interpreted as unsigned. |
| Explicit overflow behavior for the widening instructions |
to_bits_unsafe is used, although the Sail prelude says that it “should never be used.” |
(1) and (2): The Pseudocode Appears to Imply Unsigned Results
As far as I can tell, no truncation occurs at L68, L112, or L161.
to_bits
has the following type:
// Convert an integer to bits. This requires that the integer is known to fit
// inside the resulting bit vector. Note this only admits non-negative numbers.
val to_bits : forall 'l 'x, 'l >= 0 & 0 <= 'x < 2 ^ 'l .
(implicit('l), int('x)) -> bits('l)
All three call sites satisfy this constraint:
abs_int(signed(bits(SEW))) ranges over 0 .. 2^(SEW-1).
abs_int(signed(a) - signed(b)) ranges over 0 .. 2^SEW - 1.
- The unsigned
abs_diff also ranges over 0 .. 2^SEW - 1.
In every case, the upper bound is less than 2^SEW. These therefore appear to
be exact conversions rather than wrapping conversions:
to_bits(8, 128) = 0x80
to_bits(8, 255) = 0xFF
If this interpretation is correct, the apparent wraparound in case (1) comes
from how the destination is interpreted afterward, rather than from the
operation itself.
abs_int does guarantee a non-negative integer, but at the boundary that
property cannot be preserved by the bit pattern alone: when SEW=8, 128 and
-128 share the bit pattern 0x80. An SEW-bit field has no additional state
with which to carry the intended signedness.
Moreover, choosing to_bits—which “only admits non-negative numbers”—instead
of to_bits_truncate suggests that the pseudocode is already treating the
destination as an unsigned value.
A signed destination also seems difficult to reconcile with vabd.vv, since
an SEW-bit signed value cannot represent 255 when SEW=8. Consequently, a
rule covering only the most-negative vabs.v input would not be sufficient.
This asymmetry is what leads me to believe that unsigned results are intended.
If that is indeed the intent, wording along the following lines might cover
both cases:
[#norm:vabd_result_unsigned]#The results of `vabs.v`, `vabd.vv`, and
`vabdu.vv` are unsigned SEW-bit values. Consequently, when `vabs.v` is
applied to the most negative value -2^(SEW-1), it produces the unsigned
value 2^(SEW-1). These instructions do not saturate and do not modify
`vxsat`.#
This would avoid the need for a separate most-negative rule and would exclude
saturation from the interpretation. That would be consistent with RVV
convention, under which saturating operations have distinct mnemonics—such as
vsadd, vssub, and vnclip—and set vxsat. Neither characteristic applies
here.
(3): Possible Ambiguity Around to_bits_unsafe
The widening accumulators appear capable of exceeding the range of a
2*SEW-bit value. Under norm:vwabda_op and norm:vwabdau_op, the source
elements are SEW bits wide, where SEW ∈ {8, 16}, and the destination elements
are 2*SEW bits wide.
For example, when SEW=8:
This boundary is reachable in ordinary use, not merely in theory. A SAD
reduction over a 16×16 block of 8-bit samples can accumulate up to:
That value fits in 16 bits only if the destination is interpreted as unsigned.
If it is interpreted as signed, the number of maximum-difference elements that
can be accumulated without exceeding the positive range is limited to:
That is only half of a 16×16 block. Since motion estimation is listed among
the target workloads in the Introduction, it would be useful to state which
interpretation is intended.
Ordinary to_bits therefore cannot be used at these two call sites for every
possible input; some overflow behavior must be selected. This makes the use of
to_bits_unsafe understandable. My only hesitation is the following comment in
the Sail prelude:
// This version should never be used. It is only for migrating code from
// when there was no distinction between checked and truncating to_bits.
val to_bits_unsafe : forall 'l, 'l >= 0 .
(implicit('l), int) -> bits('l)
to_bits_unsafe and to_bits_truncate are identical in both signature and
implementation, differing only in that comment. It is therefore unclear
whether the use of to_bits_unsafe here is intended to specify wraparound or
is a placeholder left over from an earlier migration.
If wraparound is intended, stating that behavior in the normative prose and
replacing to_bits_unsafe with to_bits_truncate would make the semantics
unambiguous for implementers.
Thanks for considering these points.
Summary
Thanks for the recent revision. Moving the difference into unbounded integers
and applying
abs_intmakes the intermediate widths much easier to follow.One point I could not settle from the current text is that every instruction’s
source operands are qualified as signed or unsigned, whereas the destination is
qualified only by width. This leaves three related questions:
vabs.vproduce for the most negative input,-2^(SEW-1)—for example,-128whenSEW=8?vabd.vvandvabdu.vvproduce when the difference reaches2^SEW - 1—for example,255whenSEW=8?vwabda.vvandvwabdau.vvbehave when the accumulator exceedsthe range of a
2*SEW-bit value?My reading is that the pseudocode already answers (1) and (2), so those cases
may only need a sentence in the normative prose. Case (3) appears to require a
rule of its own. I would be happy to be corrected on any of these points.
(cc
@riscv/arc)Specification Reference
Permalink:
https://github.com/riscv/integer-vector-absolute-difference/blob/60b6370c6/src/unpriv/zvabd.adoc#L68
On
devat commit60b6370c6:to_bits(SEW, abs_int(signed(vs2_val[i])))to_bits(SEW, abs_int(signed(vs2_val[i]) - signed(vs1_val[i])))to_bits(SEW, abs_diff)to_bits_unsafe(SEW_widen, signed(vd_val[i]) + abs_int(diff))to_bits_unsafe(SEW_widen, unsigned(vd_val[i]) + abs_diff)The normative prose I reviewed includes:
norm:vabs_op(L58): “An absolute value operation is performed on eachelement of
vs2.”norm:vabd_op(L102) /norm:vabdu_op(L147): the operation is performed“between the elements of two signed/unsigned integer SEW-bit source operands
vs1andvs2.”norm:vwabda_op(L198) /norm:vwabdau_op(L248): the operation“accumulates the results into the elements of a
2*SEW-bit integer operandvd.”The sources are consistently qualified as signed or unsigned, while the
destination is qualified only by width. I may have missed an existing
statement, but searching the document for
most negative,saturate,overflow,wrap, andvxsatreturned no hits. I therefore believe that thesignedness of the destination is not stated anywhere.
Expected vs. Observed
vabs.v,vabd.vv, andvabdu.vvproduce unsigned SEW-bit resultsvabs.vapplied to-2^(SEW-1)to_bits(8, abs_int(-128)) = to_bits(8, 128) = 0x80; this is128when interpreted as unsigned and-128when interpreted as signed.vabd.vvat the maximum differenceto_bits(8, 255) = 0xFF, which represents the correct value only when interpreted as unsigned.to_bits_unsafeis used, although the Sail prelude says that it “should never be used.”(1) and (2): The Pseudocode Appears to Imply Unsigned Results
As far as I can tell, no truncation occurs at L68, L112, or L161.
to_bitshas the following type:
All three call sites satisfy this constraint:
abs_int(signed(bits(SEW)))ranges over0 .. 2^(SEW-1).abs_int(signed(a) - signed(b))ranges over0 .. 2^SEW - 1.abs_diffalso ranges over0 .. 2^SEW - 1.In every case, the upper bound is less than
2^SEW. These therefore appear tobe exact conversions rather than wrapping conversions:
If this interpretation is correct, the apparent wraparound in case (1) comes
from how the destination is interpreted afterward, rather than from the
operation itself.
abs_intdoes guarantee a non-negative integer, but at the boundary thatproperty cannot be preserved by the bit pattern alone: when
SEW=8,128and-128share the bit pattern0x80. An SEW-bit field has no additional statewith which to carry the intended signedness.
Moreover, choosing
to_bits—which “only admits non-negative numbers”—insteadof
to_bits_truncatesuggests that the pseudocode is already treating thedestination as an unsigned value.
A signed destination also seems difficult to reconcile with
vabd.vv, sincean SEW-bit signed value cannot represent
255whenSEW=8. Consequently, arule covering only the most-negative
vabs.vinput would not be sufficient.This asymmetry is what leads me to believe that unsigned results are intended.
If that is indeed the intent, wording along the following lines might cover
both cases:
This would avoid the need for a separate most-negative rule and would exclude
saturation from the interpretation. That would be consistent with RVV
convention, under which saturating operations have distinct mnemonics—such as
vsadd,vssub, andvnclip—and setvxsat. Neither characteristic applieshere.
(3): Possible Ambiguity Around
to_bits_unsafeThe widening accumulators appear capable of exceeding the range of a
2*SEW-bit value. Undernorm:vwabda_opandnorm:vwabdau_op, the sourceelements are SEW bits wide, where
SEW ∈ {8, 16}, and the destination elementsare
2*SEWbits wide.For example, when
SEW=8:For
vwabdau.vv(L264):For
vwabda.vv(L212):This exceeds the maximum signed 16-bit value.
This boundary is reachable in ordinary use, not merely in theory. A SAD
reduction over a
16×16block of 8-bit samples can accumulate up to:That value fits in 16 bits only if the destination is interpreted as unsigned.
If it is interpreted as signed, the number of maximum-difference elements that
can be accumulated without exceeding the positive range is limited to:
That is only half of a
16×16block. Since motion estimation is listed amongthe target workloads in the Introduction, it would be useful to state which
interpretation is intended.
Ordinary
to_bitstherefore cannot be used at these two call sites for everypossible input; some overflow behavior must be selected. This makes the use of
to_bits_unsafeunderstandable. My only hesitation is the following comment inthe Sail prelude:
to_bits_unsafeandto_bits_truncateare identical in both signature andimplementation, differing only in that comment. It is therefore unclear
whether the use of
to_bits_unsafehere is intended to specify wraparound oris a placeholder left over from an earlier migration.
If wraparound is intended, stating that behavior in the normative prose and
replacing
to_bits_unsafewithto_bits_truncatewould make the semanticsunambiguous for implementers.
Thanks for considering these points.