多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

149、MLIR的Posit格式与自定义浮点格式

149、MLIR的Posit格式与自定义浮点格式 MLIR的Posit格式与自定义浮点格式去年在做一个边缘AI加速器项目时遇到了一个让我连续加班三天的bug。模型在x86上跑FP32精度一切正常部署到我们自研的RISC-V核上推理结果直接崩了——softmax输出全是NaN。排查到最后发现是硬件单元只支持Posit格式而编译器后端在MLIR降级过程中把FP32的加法树直接映射成了硬件不支持的IEEE754指令。那一刻我意识到MLIR的类型系统虽然强大但自定义浮点格式的接入远比想象中复杂。为什么需要PositIEEE754不够用吗IEEE754的FP32在动态范围和精度之间做了折中但在某些场景下比如传感器数据融合、低功耗推理它的指数位和尾数位分配是固定的。Posit格式由John L. Gustafson提出采用了一种“regime”机制可以根据数值大小动态调整指数范围。简单说小数值时精度更高大数值时范围更广。举个例子Posit8,08位总长0个指数位在[-1,1]区间内能提供比FP8更高的精度这对神经网络激活值量化非常友好。但代价是——硬件实现复杂编译器支持几乎为零。MLIR中自定义浮点类型的接入点MLIR的浮点类型体系以FloatType为基类内置了F16、BF16、F32、F64等。要接入Posit不能直接继承FloatType——MLIR的类型系统是封闭的自定义类型必须通过Dialect扩展。我踩过的第一个坑试图在BuiltinDialect里加Posit类型。MLIR的FloatType枚举是硬编码在mlir/IR/BuiltinTypes.h里的新增一个枚举值需要修改核心库这会导致与上游版本严重脱节。正确做法是创建自己的PositDialect在其中定义PositType。// 别这样写直接继承FloatType// class PositType : public FloatType { ... }; // 编译不过// 正确姿势通过Dialect扩展classPositDialect:publicmlir::Dialect{public:explicitPositDialect(mlir::MLIRContext*ctx):Dialect(getDialectNamespace(),ctx,TypeID::getPositDialect()){addTypesPositType();}staticStringRefgetDialectNamespace(){returnposit;}};classPositType:publicmlir::Type::TypeBasePositType,mlir::Type,mlir::TypeStorage{public:usingBase::Base;// 这里踩过坑必须实现getBitWidth否则LLVM后端无法分配寄存器unsignedgetBitWidth()const{return8;}// 假设8位Posit};注意TypeStorage的定制。Posit格式的参数包括总位宽、指数位宽、是否带舍入模式。这些需要存储在TypeStorage中否则类型系统无法区分Posit8,0和Posit16,1。类型转换从FP32到Posit的降级MLIR中类型转换的核心是TypeConverter。但直接写convertType回调是不够的——你还需要处理arith、math等Dialect中的操作。我当时的做法是分两步走第一步在PositDialect中定义PositToFloat和FloatToPosit转换操作。这些操作在硬件上对应专门的转换指令。// 伪代码实际用ODS定义 def PositToFloatOp : Posit_Opposit_to_float { let arguments (ins PositType:$input); let results (outs F32:$output); let assemblyFormat $input attr-dict; }第二步写一个ConvertPositToFloatPass把arith.addf等操作中涉及Posit类型的部分拆解成PositToFloatarith.addfFloatToPosit。这里有个性能陷阱如果每个算术操作都做两次转换性能会惨不忍睹。更好的做法是让硬件直接支持Posit算术但初期为了快速验证这种“软转换”是必要的。自定义浮点格式的LLVM IR降级当MLIR降到LLVM Dialect时Posit类型必须映射到LLVM的整数类型。比如Posit8,0映射到i8。这里的关键是LLVM IR本身不感知浮点语义所有Posit算术操作都需要用内联函数或库调用实现。我写了一个PositABI接口类似LLVM的TargetLowering// 在LLVMConversionTarget中注册classPositLLVMLowering:publicmlir::LLVM::LLVMLowering{// 这里踩过坑必须处理所有涉及Posit的操作包括arith、math、vectorLogicalResultlowerOperation(Operation*op,LLVM::LLVMPatternRewriterrewriter)override{if(autoaddfdyn_castarith::AddFOp(op)){// 检查操作数类型autotypeaddf.getType().dyn_castPositType();if(!type)returnfailure();// 生成库调用__posit_add_i8(i8, i8) - i8// 别这样写直接内联实现会导致LLVM优化器无法处理// 应该生成函数调用让链接器处理autocalleegetOrInsertPositLibFunc(rewriter,__posit_add,type);rewriter.replaceOpWithNewOpLLVM::CallOp(op,callee,addf.getOperands());returnsuccess();}returnfailure();}};库函数的实现可以用C语言写然后通过LLVM的orcjit或静态链接。注意Posit格式的加法比IEEE754复杂——需要先解码regime字段对齐指数再执行尾数加法。我见过有人直接用查表法实现8位Posit加法速度比软件模拟快一个数量级。调试中的血泪教训第一个教训类型转换的合法性检查。MLIR的verify钩子默认只检查类型是否匹配不会检查Posit格式的参数是否合法。比如Posit8, 5总位宽8指数位5是无效的因为regime至少需要1位。我漏写了这个检查导致后端生成了错误的硬件配置字芯片直接挂死。第二个教训舍入模式的处理。Posit格式默认使用“round to nearest, ties to even”但硬件可能只支持截断。我在PositType的存储中加了一个RoundingMode字段然后在转换操作中显式传递。MLIR的属性系统非常适合干这个——用IntegerAttr编码舍入模式比在操作名上加后缀优雅得多。第三个教训向量化。当vector4xPosit8,0出现时LLVM后端会尝试向量化库调用。但__posit_add是标量函数LLVM的自动向量化器不会内联它。我不得不手写vector4xi8的SIMD版本用shuffle指令模拟Posit加法。这个坑让我意识到自定义浮点格式的向量化必须在MLIR层面就展开成标量操作或者提供向量化的库函数。性能调优的实战经验Posit格式的软件模拟比IEEE754慢10-100倍。在MLIR中可以通过--canonicalize和--cse来消除冗余的类型转换。比如%a posit.to_float %x : posit8,0 to f32 %b posit.to_float %y : posit8,0 to f32 %c arith.addf %a, %b : f32 %d float.to_posit %c : f32 to posit8,0如果后续还有%e float.to_posit %cCSE会复用%d。但更常见的情况是%a和%b被多次使用每次使用都重复转换。这时候需要手动做“提升”——把转换操作提到循环外面。另一个技巧利用MLIR的--convert-scf-to-cf和--convert-control-flow-to-llvm把循环内的Posit操作展开成标量序列。虽然代码体积变大但LLVM后端可以更好地做指令调度。个人经验性建议如果你正在做自定义浮点格式的MLIR接入记住三点第一不要试图修改MLIR核心类型系统。用Dialect扩展哪怕多写几行代码也比维护一个forked版本强。MLIR的版本迭代很快每次合并上游都是噩梦。第二类型转换的代价比你想象的大。在硬件不支持Posit算术的情况下尽量把转换操作推到边界——比如在加载数据时一次性转成FP32计算完再转回Posit。不要在每次算术操作前后都做转换。第三测试用例要覆盖边界值。Posit格式的regime字段在数值接近0或无穷大时行为特殊。我见过一个bug当输入为0时Posit加法返回了NaN因为regime解码逻辑把全0位模式当成了异常。写测试时用check-posit工具生成随机数值对对比软件模拟和硬件结果。最后如果你在MLIR社区提PR记得把Posit格式的规范文档附上。LLVM社区对自定义浮点格式的态度是“可以接受但必须完整”。我提交的PositDialectpatch被要求补充了类型转换的合法性检查、舍入模式文档、以及至少10个端到端测试用例。这些工作虽然繁琐但能让你的代码活得更久。
返回列表