[CPU][TRANSFORMATIONS][SNIPPETS] Keep Sin/Cos argument computation in f32 under bf16/f16 inference precision#36719
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses a CPU bf16/f16 inference accuracy regression for models that compute RoPE sin/cos tables at runtime (where RoPEFusion cannot apply) by keeping the Sin/Cos argument computation in f32 and preventing Snippets precision enforcement from collapsing the marked subgraph.
Changes:
- Added a new CPU transformation pass (
MarkSinCosInputsPrecision) to mark Sin/Cos producer cones withdisable_conversion(f16)and exclude them from Snippets tokenization. - Registered the new pass in the CPU post-LPT transformation pipeline for bf16/f16 inference precision.
- Updated
ConvertToPowerStaticto re-apply the (non-copyable) DisablePrecisionConversion marker aftercopy_runtime_info, plus added unit/functional regression tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/plugins/intel_cpu/tests/unit/transformations/mark_sin_cos_inputs_precision_test.cpp | Unit tests validating traversal/stop conditions for the new marking pass. |
| src/plugins/intel_cpu/tests/functional/custom/subgraph_tests/src/x64/rope_table_precision.cpp | Functional bf16 regression test covering runtime-computed RoPE angle stability on CPU x64. |
| src/plugins/intel_cpu/src/transformations/transformation_pipeline.cpp | Registers MarkSinCosInputsPrecision in the bf16/f16 post-LPT pipeline. |
| src/plugins/intel_cpu/src/transformations/cpu_opset/common/pass/mark_sin_cos_inputs_precision.hpp | Declares the new model pass and documents intended behavior. |
| src/plugins/intel_cpu/src/transformations/cpu_opset/common/pass/mark_sin_cos_inputs_precision.cpp | Implements the marking + Snippets-skipping traversal from Sin/Cos backward through FP producers. |
| src/plugins/intel_cpu/src/transformations/cpu_opset/common/pass/convert_to_power_static.cpp | Re-applies DisablePrecisionConversion after copy_runtime_info during PowerStatic replacement. |
|
#36718 and #36719 combined are able to fix the issue. I validated by building OpenVINO from these pull requests and running the reproduction script, obtained output is attached. Quality improved a lot, inference time was increased by about ~1 sec, but the speedup against fp32 is still almost 3.8x. ltx_t2v_output.5.mp4 |
8579c12 to
5cf8602
Compare
|
@v-Golubev , could you please review? |
|
@v-Golubev all checks passed! |
v-Golubev
left a comment
There was a problem hiding this comment.
@goyaladitya05 do I understand correctly that sin/cos layers should be kept in f32 precision not only for f32->bf16, but also for f32->f16 conversion? If yes, than this transformation and the corresponding test should be shared between CPU and GPU
| auto mark = [](const std::shared_ptr<ov::Node>& node) { | ||
| ov::disable_conversion(node, ov::element::f16); | ||
| // tokenized Subgraph bodies are precision-enforced as a whole | ||
| snippets::pass::SetSnippetsNodeType(node, snippets::pass::SnippetsNodeType::SkippedByPlugin); |
There was a problem hiding this comment.
We shouldn't disable Snippets tokenization for such nodes. The root cause is that we don't copy runtime info to a new Subgraph node during Snippets tokenization. It looks like we need to fix the Subgraph node creation in the same way as it is done in ConvertToPowerStatic, but take into account that it is a composite node
Let's check all the nodes in a body in Subgraph's constructor and if one of them has DisablePrecisionConversion attribute. copy it to the Subgraph's rt_info:
openvino/src/common/snippets/src/op/subgraph.cpp
Lines 219 to 233 in b0194f0
There was a problem hiding this comment.
afaik there's a constraint here: snippets is built before transformations, so the Subgraph constructor can't link DisablePrecisionConversion/is_conversion_disabled. Also, this is a pre-existing general gap (MarkRopeInputsToKeepInMixedPrecision has the same exposure, and tokenization_utils already copy_runtime_infos to the Subgraph but skips the non-copyable attribute).
I'd prefer to handle it as a separate snippets PR, if that works.
There was a problem hiding this comment.
This turned out to be an issue, with static shapes the angle op gets tokenized and the mark was lost. Note the Subgraph constructor can't fix it: tokenization clones nodes into the body (clone_with_new_inputs), and since DisablePrecisionConversion is non-copyable the body op no longer carries the mark. The originals do, so I propagate it from the source node(s) to the Subgraph at the creation chokepoints, build_subgraph (covers single-node wrap_node_as_subgraph) and both tokenization sites. It's copied by rt_info key because snippets can't link the transformations lib; if you'd prefer the typed API we'd need to move DisablePrecisionConversion to core.
5cf8602 to
22b369b
Compare
| auto root = m.get_match_root().get(); | ||
|
|
||
| auto mark = [](ov::Node* node) { | ||
| ov::disable_conversion(node->shared_from_this(), element::f16); |
There was a problem hiding this comment.
You're mentioning bf16 in the description, but only disabling for f16, why?
There was a problem hiding this comment.
disable_conversion(f16) protects both:
f16: ConvertPrecision(f32->f16) honors it (filter_precisions_for_node).
bf16 (CPU): node.cpp:190 -> if (is_conversion_disabled(op, element::f16)) keepOriginalPrecision = true, EnforceInferencePrecision skips it.
also, MarkRopeInputsToKeepInMixedPrecision uses the same disable_conversion(element::f16).
|
|
||
| namespace ov::pass { | ||
|
|
||
| MarkSinCosInputToKeepInMixedPrecision::MarkSinCosInputToKeepInMixedPrecision() { |
There was a problem hiding this comment.
@v-Golubev , should we make this pass a part of MarkSugraphsToKeepInMixedPrecision? What do you think?
There was a problem hiding this comment.
@v-Golubev @CuriousPanCake MarkSugraphsToKeepInMixedPrecision only runs inside ConvertPrecision when inferencePrecision == f16. This pass needs PostLpt, gated on bf16 or fp16, and just sets DisablePrecisionConversion consumed at CPU node-construction for runtime execution, the IR stays fp32. Folding it in would mean it never fires for bf16, which is what this fix also targets.
There was a problem hiding this comment.
yes, and don't we need to extend it instead and support the both inference precisions bf16 and fp16, @v-Golubev what do you think?
| auto sin_cos = pattern::wrap_type<v0::Sin, v0::Cos>(); | ||
|
|
||
| ov::matcher_pass_callback callback = [OV_CAPTURE_CPY_AND_THIS](pattern::Matcher& m) { | ||
| auto root = m.get_match_root().get(); |
There was a problem hiding this comment.
If it's already marked, do we need to enter the callback and do the marking? Or is this check omitted to traverse and mark the nodes above?
There was a problem hiding this comment.
For example, in a similar PR: #36504 there's not marking of the nodes above? I'm wondering what's the proper approach. @v-Golubev
There was a problem hiding this comment.
@CuriousPanCake, MarkRMS only marks the matched node itself, so filtering already-marked nodes out of the pattern match should be sufficient. This pass also has to mark the whole upstream angle computation, so visited is "have I already walked this subgraph," not just "is this node marked", it's the same guard MarkRopeInputsToKeepInMixedPrecision uses above. It's a perf/redundancy guard, correctness doesn't depend on it, since visit_path de-dupes internally and marking is idempotent.
|
build_jenkins |
Yes @v-Golubev, it's an fp16 concern too, and disable_conversion(f16), is honoured by both f16 ConvertPrecision and the CPU bf16 path. I've moved the pass and its test into common transformations. I've registered it only in the CPU pipeline for now, because GPU fp16 already works correctly on these models, and has its own passes. Also, MarkSubgraphsToKeepInMixedPrecision has no sin/cos rule, so a general pass could replace or consolidate the GPU ones, but I would prefer to do that in a follow up PR. |
|
The CI failure is unreleted to my changes, could we please re-run the tests? |
|
@goyaladitya05 As I understand it, these sin/cos operations should remain in FP32 even for FP32-to-FP16 conversion. On the other hand, we do not currently observe any accuracy issues on the GPU. You mentioned that the GPU has its own passes. Are these passes part of |
They're dedicated GPU-specific passes, not part of MarkSubgraphsToKeepInMixedPrecision (which has no Sin/Cos handling). In the GPU pipeline, just before ConvertPrecision(f32->f16), there's a set of narrow pattern passes that disable_conversion(f16) on specific Sin/rope structures, DisableFP16ComForGPTOSSROPEPattern, DisableFP16ComSinGenPatternForHiFiGAN, DisableFP16CompCumSumSinGen, DisableFP16CompForGemma3RMSPattern. So GPU keeps these in FP32 for F32->F16 too, per-pattern rather than generally, which is probably why there's no observed GPU issue. This pass is the general form of the same idea. @v-Golubev |
22b369b to
296a272
Compare
296a272 to
1304e20
Compare
|
@v-Golubev @CuriousPanCake could you please retrigger CI? it should pass now |
| } | ||
| toReplace->set_friendly_name(node->get_friendly_name()); | ||
| ov::copy_runtime_info(node, toReplace); | ||
| // DisablePrecisionConversion is non-copyable and dropped by copy_runtime_info, re-apply it |
There was a problem hiding this comment.
Does this mean we should make this attribute copyable? (Not a request for you)
There was a problem hiding this comment.
That would work, and it'd let us drop both workarounds here (the ConvertToPowerStatic re-apply and the snippets propagation), but it's global. every copy_runtime_info from a marked node would propagate "keep in fp32", including decomposition/fusion passes, so there's an over-marking risk (e.g. a fusion pulling the mark onto a MatMul).
If that's acceptable, I'll set flipping is_copyable() to true and removing the manual re-applies to see what happens, but preferably as a separate PR.
… f32 under mixed precision
1304e20 to
f7c4c84
Compare
|
@goyaladitya05 My concern is that a general pass may end up skipping more layers/subgraphs than actually necessary, which could potentially lead to performance degradation. Since GPU already handles this correctly with a set of targeted per-pattern passes, my suggestion would be to reuse those existing GPU passes instead of introducing a new generic pass for CPU. We could move them from the GPU-specific code into src/common/transformations/src/transformations/fp16_compression and make them available for both plugins. Then, in the CPU pipeline, these passes could be registered in 2 places:
This approach would preserve the current fine-grained behavior already proven on GPU, while avoiding the risk of disabling FP16 conversion for a broader set of subgraphs than required. |
|
@goyaladitya05 are you planning on making the changes? |
|
@CuriousPanCake @v-Golubev yes, I'm planning to do the changes, please give me a day or two so I can scope and validate them properly. |
Details:
Root cause: when RoPE tables are computed at runtime inside the model (e.g. LTX-Video's 3-axis rope, whose grid depends on width/height/num_frames inputs, so RoPEFusion cannot apply), bf16 enforcement lowers the angle computation feeding Sin/Cos to bf16. Angles reach theta·π/2 (~1.6e4 rad), where bf16's 2⁻⁸ relative precision means ~60 rad quantization steps. sin/cos of such angles is noise, scrambling the high-frequency positional components.
Confirmed by node-precision bisect on LTX-Video: keeping only the rope table in f32 restores the f32 latent trajectory.
PyTorch computes these tables in f32 unconditionally; MarkFloatingPointRange doesn't help because it stops propagating at the first arithmetic op after Range.
This PR introduces:
New MarkSinCosInputToKeepInMixedPrecision pass (common transformations): marks the floating-point producer cone of each Sin/Cos (up to Constants/Parameters/MatMul/Convolution) with disable_conversion(f16) via visit_path. The cone is cheap once-per-inference elementwise math; MatMuls are never crossed.
Snippets tokenization propagates DisablePrecisionConversion from the original node to the Subgraph, since copy_runtime_info drops the non-copyable attribute and the marking was otherwise lost when the angle op got fused.
ConvertToPowerStatic re-applies the non-copyable DisablePrecisionConversion attribute after copy_runtime_info, so the marking survives lowering to PowerStatic.
Partial fix for: #36715
AI Assistance:
used at several points. Verified by running inference using LTX-Video using original reproduction script (output attached).