Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fuzzer: Add SIMD support to DeNaN #6318

Merged
merged 15 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 90 additions & 22 deletions src/passes/DeNaN.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct DeNaN : public WalkerPass<
// Adds calls.
bool addsEffects() override { return true; }

Name deNan32, deNan64;
Name deNan32, deNan64, deNan128;

void visitExpression(Expression* expr) {
// If the expression returns a floating-point value, ensure it is not a
Expand Down Expand Up @@ -67,6 +67,13 @@ struct DeNaN : public WalkerPass<
} else {
replacement = builder.makeCall(deNan64, {expr}, Type::f64);
}
} else if (expr->type == Type::v128) {
if (c && hasNaNLane(c)) {
uint8_t zero[16] = {};
replacement = builder.makeConst(Literal(zero));
} else {
replacement = builder.makeCall(deNan128, {expr}, Type::v128);
}
}
if (replacement) {
// We can't do this outside of a function, like in a global initializer,
Expand Down Expand Up @@ -98,6 +105,11 @@ struct DeNaN : public WalkerPass<
i,
builder.makeCall(
deNan64, {builder.makeLocalGet(i, Type::f64)}, Type::f64)));
} else if (func->getLocalType(i) == Type::v128) {
fixes.push_back(builder.makeLocalSet(
i,
builder.makeCall(
deNan128, {builder.makeLocalGet(i, Type::v128)}, Type::v128)));
}
}
if (!fixes.empty()) {
Expand All @@ -115,34 +127,90 @@ struct DeNaN : public WalkerPass<
// Pick names for the helper functions.
deNan32 = Names::getValidFunctionName(*module, "deNan32");
deNan64 = Names::getValidFunctionName(*module, "deNan64");
deNan128 = Names::getValidFunctionName(*module, "deNan128");

ControlFlowWalker<DeNaN, UnifiedExpressionVisitor<DeNaN>>::doWalkModule(
module);

// Add helper functions after the walk, so they are not instrumented.
addFunc(module, deNan32, Type::f32, Literal(float(0)), EqFloat32);
addFunc(module, deNan64, Type::f64, Literal(double(0)), EqFloat64);

if (module->features.hasSIMD()) {
uint8_t zero128[16] = {};
addFunc(module, deNan128, Type::v128, Literal(zero128));
}
}

// Add a de-NaN-ing helper function.
void addFunc(Module* module,
Name name,
Type type,
Literal literal,
std::optional<BinaryOp> op = {}) {
Builder builder(*module);
auto add = [&](Name name, Type type, Literal literal, BinaryOp op) {
auto func = Builder::makeFunction(name, Signature(type, type), {});
// Compare the value to itself to check if it is a NaN, and return 0 if
// so:
auto func = Builder::makeFunction(name, Signature(type, type), {});
// Compare the value to itself to check if it is a NaN, and return 0 if
// so:
//
// (if (result f*)
// (f*.eq
// (local.get $0)
// (local.get $0)
// )
// (local.get $0)
// (f*.const 0)
// )
Expression* condition;
if (type != Type::v128) {
// Generate a simple condition.
assert(op);
condition = builder.makeBinary(
*op, builder.makeLocalGet(0, type), builder.makeLocalGet(0, type));
} else {
assert(!op);
// v128 is trickier as the 128 bits may contain f32s or f64s, and we
// need to check for nans both ways in principle. However, the f32 NaN
// pattern is a superset of f64, since it checks less bits (8 bit
// exponent vs 11), and it is checked in more places (4 32-bit values vs
// 2 64-bit ones), so we can just check that. That is, this reduces to 4
// checks of f32s, but is otherwise the same as a check of a single f32.
//
// (if (result f*)
// (f*.eq
// (local.get $0)
// (local.get $0)
// )
// (local.get $0)
// (f*.const 0)
// )
func->body = builder.makeIf(
builder.makeBinary(
op, builder.makeLocalGet(0, type), builder.makeLocalGet(0, type)),
builder.makeLocalGet(0, type),
builder.makeConst(literal));
module->addFunction(std::move(func));
};
add(deNan32, Type::f32, Literal(float(0)), EqFloat32);
add(deNan64, Type::f64, Literal(double(0)), EqFloat64);
// However there is additional complexity, which is that if we do
// EqVecF32x4 then we get all-1s for each case where we compare equal.
// That itself is a NaN pattern, which means that running this pass
// twice would interfere with itself. To avoid that we'd need a way to
// detect our previous instrumentation and not instrument it, but that
// is tricky (we can't depend on function names etc. while fuzzing).
// Instead, extract the lanes and use f32 checks.
auto getLane = [&](Index index) {
return builder.makeSIMDExtract(
ExtractLaneVecF32x4, builder.makeLocalGet(0, type), index);
};
auto getLaneCheck = [&](Index index) {
return builder.makeBinary(EqFloat32, getLane(index), getLane(index));
};
auto* firstTwo =
builder.makeBinary(AndInt32, getLaneCheck(0), getLaneCheck(1));
auto* lastTwo =
builder.makeBinary(AndInt32, getLaneCheck(2), getLaneCheck(3));
condition = builder.makeBinary(AndInt32, firstTwo, lastTwo);
}
func->body = builder.makeIf(
condition, builder.makeLocalGet(0, type), builder.makeConst(literal));
module->addFunction(std::move(func));
};

// Check if a contant v128 may contain f32 or f64 NaNs.
bool hasNaNLane(Const* c) {
assert(c->type == Type::v128);
auto value = c->value;

// Compute if all f32s are equal to themselves.
auto test32 = value.eqF32x4(value);
test32 = test32.allTrueI32x4();

return !test32.getInteger();
}
};

Expand Down
131 changes: 131 additions & 0 deletions test/lit/passes/denan-simd.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.

;; RUN: foreach %s %t wasm-opt --denan -all -S -o - | filecheck %s

(module
;; CHECK: (type $0 (func (param v128) (result v128)))

;; CHECK: (type $1 (func (param f32) (result f32)))

;; CHECK: (type $2 (func (param f64) (result f64)))

;; CHECK: (func $foo128 (type $0) (param $x v128) (result v128)
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (call $deNan128
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (call $deNan128
;; CHECK-NEXT: (v128.const i32x4 0x00000001 0x00000002 0x00000003 0x00000004)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000)
;; CHECK-NEXT: )
;; CHECK-NEXT: (call $deNan128
;; CHECK-NEXT: (call $foo128
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $foo128 (param $x v128) (result v128)
;; The incoming param will be de-naned.

;; This is not a NaN. (We do still emit a call for it atm, FIXME)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this happen?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason - that's what the code did for f32 and f64 before this PR. I can fix it later.

(drop
(v128.const i32x4 0x00000001 0x00000002 0x00000003 0x00000004)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful to write the tests using f32x4 and f64x2 shapes rather than i32x4 so we can better see what values are being passed. You can use the --new-wat-parser and its extensive support for the standard nan formats if you want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually thinking it is nice to see the actual bits because of how they are used in the pass (the explanation about the f32 and f64 bits overlapping etc.)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more specific, in this test code:

    ;; This is an f64 NaN and also an f32. It will become 0's.
    (drop
      (v128.const i32x4 0xffffffff 0x00000002 0x00000003 0x00000004)
    )
    ;; This is an f32 NaN and not an f64. It will also become 0's.
    (drop
      (v128.const i32x4 0x00000001 0xffffffff 0x00000003 0x00000004)
    )

It is nice to see the bits, because that allows the comments to clarify how the same bits can be both f32 and f64 nans, or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be more convinced by this if readers were likely to know off the top of their heads what bit patterns correspond to NaNs. I know I don't :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I don't know that either, but I mentioned it in the pass (8 vs 11 top non-sign bits).

)
;; This is an f64 NaN and also an f32. It will become 0's.
(drop
(v128.const i32x4 0xffffffff 0x00000002 0x00000003 0x00000004)
)
;; This is an f32 NaN and not an f64. It will also become 0's.
(drop
(v128.const i32x4 0x00000001 0xffffffff 0x00000003 0x00000004)
)

(call $foo128 (local.get $x))
)
)
;; CHECK: (func $deNan32 (type $1) (param $0 f32) (result f32)
;; CHECK-NEXT: (if (result f32)
;; CHECK-NEXT: (f32.eq
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (then
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (else
;; CHECK-NEXT: (f32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )

;; CHECK: (func $deNan64 (type $2) (param $0 f64) (result f64)
;; CHECK-NEXT: (if (result f64)
;; CHECK-NEXT: (f64.eq
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (then
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (else
;; CHECK-NEXT: (f64.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )

;; CHECK: (func $deNan128 (type $0) (param $0 v128) (result v128)
;; CHECK-NEXT: (if (result v128)
;; CHECK-NEXT: (i32.and
;; CHECK-NEXT: (i32.and
;; CHECK-NEXT: (f32.eq
;; CHECK-NEXT: (f32x4.extract_lane 0
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (f32x4.extract_lane 0
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (f32.eq
;; CHECK-NEXT: (f32x4.extract_lane 1
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (f32x4.extract_lane 1
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.and
;; CHECK-NEXT: (f32.eq
;; CHECK-NEXT: (f32x4.extract_lane 2
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (f32x4.extract_lane 2
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (f32.eq
;; CHECK-NEXT: (f32x4.extract_lane 3
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (f32x4.extract_lane 3
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (then
;; CHECK-NEXT: (local.get $0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (else
;; CHECK-NEXT: (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
Loading