[Codegen] Use overflow-checked math in the CPU and GPU alloc size checks (#24745)
`checkStackAllocationSize` accumulates the allocation size in a plain
int64_t. Allocas sized by the upper bound of an unbounded dynamic
dimension (the util.assume.int default, 2^53-1) overflow it, the wrapped
value passes the limit check and the alloca reaches ConvertToLLVM, where
its element count folds to llvm.mlir.poison and the program loads and
stores through a garbage pointer at runtime.
The reproduction was reduced from the quantized ResNet downsample
construct: an ONNX model with a dynamic batch doing DequantizeLinear ->
1x1 stride-2 Conv compiles without diagnostics and returns completely
wrong results.
Move `getStaticShapeSizeInBits` to Codegen/Utils (taken from
https://github.com/iree-org/iree/pull/24684) and use it from both
LLVMCPUCheckIRBeforeLLVMConversion and GPUCheckResourceUsage. Callers
supply the leaf element bit width,
which lets the GPU check keep its index-bitwidth handling and the
recursion into shaped element types. On the CPU side the remaining
scalar steps are overflow-checked in place with `llvm::MulOverflow` /
`llvm::AddOverflow`. The pre-existing
`checkedAdd`/`checkedMul`/`checkedAlignTo` wrappers in
LLVMCPUAssignWorkgroupLocalMemory.cpp are replaced with
`llvm::MulOverflow` / `llvm::AddOverflow`.
---------
Signed-off-by: Zmicier Prybysh <zprybysh@baylibre.com>
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/GPUCheckResourceUsage.cpp b/compiler/src/iree/compiler/Codegen/Common/GPU/GPUCheckResourceUsage.cpp
index 1083699..153c093 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/GPUCheckResourceUsage.cpp
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/GPUCheckResourceUsage.cpp
@@ -6,7 +6,9 @@
#include "iree/compiler/Codegen/Common/GPU/Passes.h"
#include "iree/compiler/Codegen/Utils/GPUUtils.h"
+#include "iree/compiler/Codegen/Utils/Utils.h"
#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/MathExtras.h"
#include "mlir/Conversion/LLVMCommon/LoweringOptions.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Interfaces/FunctionInterfaces.h"
@@ -24,32 +26,6 @@
return options.getIndexBitwidth();
}
-static int shapedTypeStaticSize(
- memref::AllocOp allocOp, ShapedType shapedType,
- std::function<unsigned(mlir::FunctionOpInterface)> getIndexBitwidth) {
- int allocSize = 1;
- for (auto dimSize : shapedType.getShape()) {
- if (ShapedType::isDynamic(dimSize)) {
- continue;
- }
- allocSize *= dimSize;
- }
- if (auto elementType = dyn_cast<ShapedType>(shapedType.getElementType())) {
- allocSize *= shapedTypeStaticSize(allocOp, elementType, getIndexBitwidth);
- } else {
- auto eltTy = shapedType.getElementType();
- if (eltTy.isIndex()) {
- auto func = allocOp->getParentOfType<mlir::FunctionOpInterface>();
- assert(getIndexBitwidth &&
- "getIndexBitwidth should have been set earlier");
- allocSize *= getIndexBitwidth(func);
- } else {
- allocSize *= IREE::Util::getTypeBitWidth(shapedType.getElementType());
- }
- }
- return allocSize;
-}
-
/// Returns success if the total shared memory allocation size is less than the
/// limit.
static LogicalResult checkGPUAllocationSize(
@@ -65,7 +41,7 @@
return success();
}
- int cumSize = 0;
+ int64_t cumSize = 0;
for (auto allocOp : allocOps) {
auto allocType = cast<MemRefType>(allocOp.getType());
if (!hasSharedMemoryAddressSpace(allocType)) {
@@ -77,13 +53,32 @@
"has unsupported dynamic shared memory allocations");
}
- int allocSize = shapedTypeStaticSize(allocOp, allocType, getIndexBitwidth);
+ FailureOr<int64_t> allocSizeBits = getStaticShapeSizeInBits(
+ allocType, [funcOp, getIndexBitwidth](Type elementType) -> int64_t {
+ if (elementType.isIndex()) {
+ assert(getIndexBitwidth && "getIndexBitwidth must not be null");
+ return getIndexBitwidth(funcOp);
+ }
+ return IREE::Util::getTypeBitWidth(elementType);
+ });
+ if (failed(allocSizeBits)) {
+ return allocOp.emitOpError(
+ "shared memory allocation size overflows 64 bits");
+ }
+ int64_t allocSize = *allocSizeBits;
if (allocOp.getAlignment()) {
int64_t alignmentInBits = *allocOp.getAlignment() * 8;
- allocSize =
- (llvm::divideCeil(allocSize, alignmentInBits) * alignmentInBits);
+ int64_t alignedUnits = llvm::divideCeil(allocSize, alignmentInBits);
+ if (llvm::MulOverflow(alignedUnits, alignmentInBits, allocSize)) {
+ return allocOp.emitOpError(
+ "shared memory allocation size overflows 64 bits");
+ }
}
- cumSize += allocSize / 8;
+ int64_t allocSizeBytes = llvm::divideCeil(allocSize, 8);
+ if (llvm::AddOverflow(cumSize, allocSizeBytes, cumSize)) {
+ return allocOp.emitOpError(
+ "cumulative shared memory allocation size overflows 64 bits");
+ }
}
if (cumSize > limit) {
return emitError(funcOp->getLoc())
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_check_resource_usage.mlir b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_check_resource_usage.mlir
index f2d7804..ffb8b15 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_check_resource_usage.mlir
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_check_resource_usage.mlir
@@ -29,3 +29,81 @@
return
}
}
+
+// -----
+
+module {
+ // expected-error @+1 {{uses 65600 bytes of shared memory; exceeded the limit of 65536 bytes}}
+ func.func @shared_mem_alloc_nested_shaped_element() {
+ memref.alloc() : memref<1025xvector<16xf32>, #gpu.address_space<workgroup>>
+ return
+ }
+}
+
+// -----
+
+module {
+ // expected-error @+1 {{uses 65537 bytes of shared memory; exceeded the limit of 65536 bytes}}
+ func.func @shared_mem_alloc_sub_byte_rounds_up() {
+ memref.alloc() : memref<524289xi1, #gpu.address_space<workgroup>>
+ return
+ }
+}
+
+// -----
+
+module {
+ func.func @shared_mem_alloc_size_overflow() {
+ // expected-error @+1 {{shared memory allocation size overflows 64 bits}}
+ memref.alloc() : memref<9007199254740991x1024x14x14xf32, #gpu.address_space<workgroup>>
+ return
+ }
+}
+
+// -----
+
+module {
+ func.func @shared_mem_alloc_index_overflow() {
+ // expected-error @+1 {{shared memory allocation size overflows 64 bits}}
+ memref.alloc() : memref<144115188075855872xindex, #gpu.address_space<workgroup>>
+ return
+ }
+}
+
+// -----
+
+module {
+ func.func @shared_mem_alloc_nested_shaped_element_overflow() {
+ // expected-error @+1 {{shared memory allocation size overflows 64 bits}}
+ memref.alloc() : memref<9007199254740991xvector<1024x14x14xf32>, #gpu.address_space<workgroup>>
+ return
+ }
+}
+
+// -----
+
+module {
+ func.func @shared_mem_alloc_alignment_overflow() {
+ // expected-error @+1 {{shared memory allocation size overflows 64 bits}}
+ memref.alloc() alignment = 64 : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ return
+ }
+}
+
+// -----
+
+module {
+ func.func @shared_mem_alloc_cumulative_overflow() {
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ // expected-error @+1 {{cumulative shared memory allocation size overflows 64 bits}}
+ memref.alloc() : memref<1152921504606846975xi8, #gpu.address_space<workgroup>>
+ return
+ }
+}
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUAssignWorkgroupLocalMemory.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUAssignWorkgroupLocalMemory.cpp
index d75b100..70929ef 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUAssignWorkgroupLocalMemory.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUAssignWorkgroupLocalMemory.cpp
@@ -49,32 +49,6 @@
memRefType.getMemorySpace());
}
-static LogicalResult checkedAdd(int64_t lhs, int64_t rhs, int64_t &result) {
- if (llvm::AddOverflow(lhs, rhs, result)) {
- return failure();
- }
- return success();
-}
-
-static LogicalResult checkedMul(int64_t lhs, int64_t rhs, int64_t &result) {
- if (llvm::MulOverflow(lhs, rhs, result)) {
- return failure();
- }
- return success();
-}
-
-static LogicalResult checkedAlignTo(int64_t value, int64_t alignment,
- int64_t &result) {
- assert(value >= 0);
- assert(alignment > 0);
- int64_t remainder = value % alignment;
- if (remainder == 0) {
- result = value;
- return success();
- }
- return checkedAdd(value, alignment - remainder, result);
-}
-
static bool hasZeroElementShape(MemRefType type) {
return llvm::is_contained(type.getShape(), 0);
}
@@ -115,15 +89,15 @@
int64_t maxElementOffset = offset;
for (auto [dim, stride] : llvm::zip_equal(type.getShape(), strides)) {
int64_t contribution = 0;
- if (failed(checkedMul(dim - 1, stride, contribution)) ||
- failed(checkedAdd(maxElementOffset, contribution, maxElementOffset))) {
+ if (llvm::MulOverflow(dim - 1, stride, contribution) ||
+ llvm::AddOverflow(maxElementOffset, contribution, maxElementOffset)) {
allocOp.emitOpError("workgroup local memory allocation size overflow");
return failure();
}
}
int64_t footprint = 0;
- if (failed(checkedAdd(maxElementOffset, 1, footprint))) {
+ if (llvm::AddOverflow(maxElementOffset, int64_t(1), footprint)) {
allocOp.emitOpError("workgroup local memory allocation size overflow");
return failure();
}
@@ -154,7 +128,7 @@
int64_t elementBytes = static_cast<int64_t>(elementByteSize.getFixedValue());
int64_t totalBytes = 0;
- if (failed(checkedMul(*elementFootprint, elementBytes, totalBytes))) {
+ if (llvm::MulOverflow(*elementFootprint, elementBytes, totalBytes)) {
allocOp.emitOpError("workgroup local memory allocation size overflow");
return failure();
}
@@ -281,7 +255,11 @@
if (failed(alignment)) {
return signalPassFailure();
}
- if (failed(checkedAlignTo(currentOffset, *alignment, currentOffset))) {
+ assert(currentOffset >= 0 && *alignment > 0);
+ int64_t misalignment = currentOffset % *alignment;
+ if (misalignment != 0 &&
+ llvm::AddOverflow(currentOffset, *alignment - misalignment,
+ currentOffset)) {
allocOp.emitOpError("workgroup local memory allocation size overflow");
return signalPassFailure();
}
@@ -291,8 +269,8 @@
/*elementFootprint=*/allocationSize->elementFootprint,
});
- if (failed(checkedAdd(currentOffset, allocationSize->byteSize,
- currentOffset))) {
+ if (llvm::AddOverflow(currentOffset, allocationSize->byteSize,
+ currentOffset)) {
allocOp.emitOpError("workgroup local memory allocation size overflow");
return signalPassFailure();
}
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUCheckIRBeforeLLVMConversion.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUCheckIRBeforeLLVMConversion.cpp
index a06f507..024ac59 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUCheckIRBeforeLLVMConversion.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUCheckIRBeforeLLVMConversion.cpp
@@ -8,6 +8,7 @@
#include "iree/compiler/Codegen/LLVMCPU/Utils.h"
#include "iree/compiler/Codegen/Utils/Utils.h"
#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/MathExtras.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/Vector/IR/ScalableValueBoundsConstraintSet.h"
#include "mlir/Interfaces/ValueBoundsOpInterface.h"
@@ -71,14 +72,15 @@
"all stack allocations need to be hoisted to the entry block of the "
"function");
}
- int64_t allocaSize = 1;
auto allocaType = cast<ShapedType>(allocaOp.getType());
- for (auto dimSize : allocaType.getShape()) {
- if (ShapedType::isDynamic(dimSize)) {
- continue;
- }
- allocaSize *= dimSize;
+ FailureOr<int64_t> staticSizeBits =
+ getStaticShapeSizeInBits(allocaType, [](Type elementType) -> int64_t {
+ return IREE::Util::getTypeBitWidth(elementType);
+ });
+ if (failed(staticSizeBits)) {
+ return allocaOp.emitOpError("stack allocation size overflows 64 bits");
}
+ int64_t allocaSize = *staticSizeBits;
for (auto operand : allocaOp.getDynamicSizes()) {
// Assume vscale is `clAssumedVscaleValue` for determining if the alloca
// is within the stack limit. This should always resolve to a constant
@@ -90,18 +92,28 @@
/*vscaleMin=*/assumedVscale,
/*vscaleMax=*/assumedVscale, presburger::BoundType::UB);
if (succeeded(ub)) {
- allocaSize *= ub->getSize()->baseSize;
+ if (llvm::MulOverflow(allocaSize,
+ static_cast<int64_t>(ub->getSize()->baseSize),
+ allocaSize)) {
+ return allocaOp.emitOpError(
+ "stack allocation size overflows 64 bits");
+ }
continue;
}
return allocaOp.emitOpError("expected no unbounded stack allocations");
}
- allocaSize *= IREE::Util::getTypeBitWidth(allocaType.getElementType());
if (allocaOp.getAlignment()) {
int64_t alignmentInBits = *allocaOp.getAlignment() * 8;
- allocaSize =
- (llvm::divideCeil(allocaSize, alignmentInBits) * alignmentInBits);
+ int64_t alignedUnits = llvm::divideCeil(allocaSize, alignmentInBits);
+ if (llvm::MulOverflow(alignedUnits, alignmentInBits, allocaSize)) {
+ return allocaOp.emitOpError("stack allocation size overflows 64 bits");
+ }
}
- cumSize += allocaSize / 8;
+ int64_t allocaSizeBytes = llvm::divideCeil(allocaSize, 8);
+ if (llvm::AddOverflow(cumSize, allocaSizeBytes, cumSize)) {
+ return allocaOp.emitOpError(
+ "cumulative stack allocation size overflows 64 bits");
+ }
}
if (cumSize > maxAllocationSizeInBytes) {
return funcOp.emitOpError("exceeded stack allocation limit of ")
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/check_ir_before_llvm_conversion.mlir b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/check_ir_before_llvm_conversion.mlir
index 5d52002..fb9e014 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/check_ir_before_llvm_conversion.mlir
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/check_ir_before_llvm_conversion.mlir
@@ -36,6 +36,56 @@
// -----
+// expected-error @+1 {{exceeded stack allocation limit of 32768 bytes for function. Got 32769 bytes}}
+func.func @sub_byte_alloca_rounds_up() {
+ %0 = memref.alloca() : memref<262145xi1>
+ return
+}
+
+// -----
+
+func.func @overflowing_static_alloca() {
+ // expected-error @+1 {{stack allocation size overflows 64 bits}}
+ %0 = memref.alloca() : memref<9007199254740991x1024x14x14xf32>
+ return
+}
+
+// -----
+
+func.func @overflowing_dynamic_alloca(%arg0: index) {
+ %0 = util.assume.int %arg0<umin = 0, umax = 9007199254740991> : index
+ // expected-error @+1 {{stack allocation size overflows 64 bits}}
+ %1 = memref.alloca(%0) : memref<?x1024x14x14xf32>
+ return
+}
+
+// -----
+
+// 9 x (2^63-8 bits = 2^60-1 bytes) > 2^63-1 bytes
+func.func @cumulative_overflowing_allocas() {
+ %0 = memref.alloca() : memref<1152921504606846975xi8>
+ %1 = memref.alloca() : memref<1152921504606846975xi8>
+ %2 = memref.alloca() : memref<1152921504606846975xi8>
+ %3 = memref.alloca() : memref<1152921504606846975xi8>
+ %4 = memref.alloca() : memref<1152921504606846975xi8>
+ %5 = memref.alloca() : memref<1152921504606846975xi8>
+ %6 = memref.alloca() : memref<1152921504606846975xi8>
+ %7 = memref.alloca() : memref<1152921504606846975xi8>
+ // expected-error @+1 {{cumulative stack allocation size overflows 64 bits}}
+ %8 = memref.alloca() : memref<1152921504606846975xi8>
+ return
+}
+
+// -----
+
+func.func @overflowing_aligned_alloca() {
+ // expected-error @+1 {{stack allocation size overflows 64 bits}}
+ %0 = memref.alloca() alignment = 64 : memref<1152921504606846975xi8>
+ return
+}
+
+// -----
+
func.func @non_entry_bb_allocas() {
cf.br ^bb1
^bb1() :
diff --git a/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp b/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp
index 34d30e4..152eaa7 100644
--- a/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp
+++ b/compiler/src/iree/compiler/Codegen/Utils/Utils.cpp
@@ -22,6 +22,7 @@
#include "llvm/ADT/TypeSwitch.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/DebugLog.h"
+#include "llvm/Support/MathExtras.h"
#include "mlir/Analysis/DataFlow/IntegerRangeAnalysis.h"
#include "mlir/Analysis/DataFlowFramework.h"
#include "mlir/Analysis/SliceAnalysis.h"
@@ -94,6 +95,31 @@
return func.isPublic() && getEntryPoint(func);
}
+FailureOr<int64_t>
+getStaticShapeSizeInBits(ShapedType shapedType,
+ llvm::function_ref<int64_t(Type)> getElementBitWidth) {
+ int64_t size = 1;
+ for (int64_t dimSize : shapedType.getShape()) {
+ if (ShapedType::isDynamic(dimSize)) {
+ continue;
+ }
+ if (llvm::MulOverflow(size, dimSize, size)) {
+ return failure();
+ }
+ }
+ Type elementType = shapedType.getElementType();
+ if (auto shapedElementType = dyn_cast<ShapedType>(elementType)) {
+ FailureOr<int64_t> elementBits =
+ getStaticShapeSizeInBits(shapedElementType, getElementBitWidth);
+ if (failed(elementBits) || llvm::MulOverflow(size, *elementBits, size)) {
+ return failure();
+ }
+ } else if (llvm::MulOverflow(size, getElementBitWidth(elementType), size)) {
+ return failure();
+ }
+ return size;
+}
+
std::optional<StringRef> getConfigCpuFeatures(DictionaryAttr targetConfig) {
auto attr = targetConfig.getAs<StringAttr>(kCpuFeaturesAttrName);
if (attr) {
diff --git a/compiler/src/iree/compiler/Codegen/Utils/Utils.h b/compiler/src/iree/compiler/Codegen/Utils/Utils.h
index a6a32f1..782cee2 100644
--- a/compiler/src/iree/compiler/Codegen/Utils/Utils.h
+++ b/compiler/src/iree/compiler/Codegen/Utils/Utils.h
@@ -46,6 +46,18 @@
std::optional<IREE::HAL::ExecutableExportOp>
getEntryPoint(mlir::FunctionOpInterface funcOp);
+/// Returns the static size in bits of `shapedType` -- the product of its
+/// static extents and the element size -- accumulated with 64-bit overflow
+/// checking. Dynamic dimensions are skipped; callers apply their own policy
+/// for those. `getElementBitWidth` returns the bit width of a leaf
+/// (non-shaped) element type, letting callers control e.g. the target's
+/// index-type width. Returns failure on integer overflow: such an allocation
+/// necessarily exceeds any real resource limit, so callers should treat
+/// failure as "over the limit".
+FailureOr<int64_t>
+getStaticShapeSizeInBits(ShapedType shapedType,
+ llvm::function_ref<int64_t(Type)> getElementBitWidth);
+
/// Returns the dispatch_config op for the `funcOp` by looking up the parent
/// module for a matching function_ref. Returns nullptr if not found.
IREE::Codegen::DispatchConfigOp