Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
63 changes: 63 additions & 0 deletions llvm/docs/LangRef.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21406,6 +21406,69 @@ environment <floatenv>` *except* for the rounding mode.
This intrinsic is not supported on all targets. Some targets may not support
all rounding modes.

'``llvm.arbitrary.fp.convert``' Intrinsic
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Syntax:
"""""""

::

declare <type> @llvm.arbitrary.fp.convert(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intrinsic is a bit omnipotent, but I haven't figured out a better way to split its functionality - the second best approach I had in mind is to split saturation and conversions with integers, but number of possible combinations were a bit too big for my taste.

Copy link
Contributor Author

@MrSidims MrSidims Nov 2, 2025

Choose a reason for hiding this comment

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

Resolved: 1 intrinsic -> 2 intrinsics with no integer conversions.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the arbitrary in the name

Copy link
Contributor Author

@MrSidims MrSidims Oct 20, 2025

Choose a reason for hiding this comment

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

Yeah, naming is questionable. Do you find mxfp.convert or mini.float.convert better naming?

<type> <value>, metadata <result interpretation>,
metadata <input interpretation>, metadata <rounding mode>,
i32 <saturation>)

Overview:
"""""""""

The ``llvm.arbitrary.fp.convert`` intrinsic performs conversions
between values whose interpretation differs from their representation
in LLVM IR. The intrinsic is overloaded on both its return type and first
argument. Metadata operands describe how the raw bits should be interpreted
before and after the conversion.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd expect to have 2 intrinsics. One uses the natural IR fp type as the source, and the other for the result. Do you need the exotic-fp-to-other-exotic-FP case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you need the exotic-fp-to-other-exotic-FP case?

I can imagine such use case

Copy link
Contributor

Choose a reason for hiding this comment

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

If there are 2 intrinsics, then it's simpler to avoid overlap with existing instructions for IR -> IR (which may be a good thing?)
Isel should be able to fold the 2 into a single exotic->exotic instruction if available for the target no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI: we (Intel) have a use case for FP4 -> FP8 conversion. Spec itself is covered by intel/llvm#20467 . But I'm changing the patch to have 2 intrinsics anyway, it indeed simplifies the semantic.


Arguments:
""""""""""

``value``
The value to convert. Its interpretation is described by ``input
interpretation``.

``result interpretation``
A metadata string that describes the type of the result. The string
can be ``"none"`` (no conversion needed), ``"signed"`` or ``"unsigned"`` (for
integer types), or any target-specific string for floating-point formats.
For example ``"spv.E4M3EXT"`` and ``"spv.E5M2EXT"`` stand for FP8 SPIR-V formats.
Using ``"none"`` indicates the converted bits already have the desired LLVM IR type.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively we can use names from https://github.com/llvm/llvm-project/blob/main/llvm/lib/Support/APFloat.cpp#L133 and check for them in the verifier. I don't have a strong opinion about that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer using the APFloat names.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now APFloat names are used


``input interpretation``
Mirrors ``result interpretation`` but applies to the first argument. The
interpretation is target-specific and describes how to interpret the raw bits
of the input value.

``rounding mode``
A metadata string. The permitted strings match those accepted by
:ref:`llvm.fptrunc.round <int_fptrunc_round>` (for example,
``"round.tonearest"`` or ``"round.towardzero"``). The string ``"none"`` may be
used to indicate that the default rounding behaviour of the conversion should
be used.

``saturation``
An integer constant (0 or 1) indicating whether saturation should be applied
to the conversion. When set to 1, values outside the representable range of
the result type are clamped to the minimum or maximum representable value
instead of wrapping. When set to 0, no saturation is applied.

Semantics:
""""""""""

The intrinsic interprets the first argument according to ``input
interpretation``, applies the requested rounding mode and saturation behavior,
and produces a value whose type is described by ``result interpretation``.
When saturation is enabled, values that exceed the representable range of the target
format are clamped to the minimum or maximum representable value of that format.

Copy link
Contributor

Choose a reason for hiding this comment

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

This could really use some examples of different type combinations.

As I understand it, there are basically three overloads here a) IR FP type, b) IR integer type, c) IR integer type interpreted as FP type.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 again, I was thinking that some examples would be helpful.

Convergence Intrinsics
----------------------

Expand Down
8 changes: 8 additions & 0 deletions llvm/include/llvm/IR/Intrinsics.td
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,14 @@ let IntrProperties = [IntrNoMem, IntrSpeculatable] in {
def int_fptrunc_round : DefaultAttrsIntrinsic<[ llvm_anyfloat_ty ],
[ llvm_anyfloat_ty, llvm_metadata_ty ]>;

// Convert between arbitrary interpreted floating-point and integer values.
def int_arbitrary_fp_convert
: DefaultAttrsIntrinsic<
[ llvm_any_ty ],
[ llvm_any_ty, llvm_metadata_ty, llvm_metadata_ty,
llvm_metadata_ty, llvm_i32_ty ],
[ IntrNoMem, IntrSpeculatable ]>;

def int_canonicalize : DefaultAttrsIntrinsic<[llvm_anyfloat_ty], [LLVMMatchType<0>],
[IntrNoMem]>;
// Arithmetic fence intrinsic.
Expand Down
5 changes: 4 additions & 1 deletion llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2842,7 +2842,10 @@ bool IRTranslator::translateCall(const User &U, MachineIRBuilder &MIRBuilder) {
if (!MDN) {
if (auto *ConstMD = dyn_cast<ConstantAsMetadata>(MD))
MDN = MDNode::get(MF->getFunction().getContext(), ConstMD);
else // This was probably an MDString.
else if (auto *MDS = dyn_cast<MDString>(MD)) {
Metadata *Ops[] = {MDS};
MDN = MDNode::get(MF->getFunction().getContext(), Ops);
} else
return false;
}
MIB.addMetadata(MDN);
Expand Down
47 changes: 47 additions & 0 deletions llvm/lib/IR/Verifier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
#include "llvm/IR/Dominators.h"
#include "llvm/IR/EHPersonalities.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/FPEnv.h"
#include "llvm/IR/GCStrategy.h"
#include "llvm/IR/GlobalAlias.h"
#include "llvm/IR/GlobalValue.h"
Expand Down Expand Up @@ -5848,6 +5849,52 @@ void Verifier::visitIntrinsicCall(Intrinsic::ID ID, CallBase &Call) {
"unsupported rounding mode argument", Call);
break;
}
case Intrinsic::arbitrary_fp_convert: {
auto *ResultMAV = dyn_cast<MetadataAsValue>(Call.getArgOperand(1));
Check(ResultMAV, "missing result interpretation metadata operand", Call);
auto *ResultStr = dyn_cast<MDString>(ResultMAV->getMetadata());
Check(ResultStr, "result interpretation metadata operand must be a string",
Call);
StringRef ResultInterp = ResultStr->getString();

auto *InputMAV = dyn_cast<MetadataAsValue>(Call.getArgOperand(2));
Check(InputMAV, "missing input interpretation metadata operand", Call);
auto *InputStr = dyn_cast<MDString>(InputMAV->getMetadata());
Check(InputStr, "input interpretation metadata operand must be a string",
Call);
StringRef InputInterp = InputStr->getString();

auto *RoundingMAV = dyn_cast<MetadataAsValue>(Call.getArgOperand(3));
Check(RoundingMAV, "missing rounding mode metadata operand", Call);
auto *RoundingStr = dyn_cast<MDString>(RoundingMAV->getMetadata());
Check(RoundingStr, "rounding mode metadata operand must be a string",
Call);
StringRef RoundingInterp = RoundingStr->getString();
Copy link
Contributor

Choose a reason for hiding this comment

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

We are doing the same thing three times here. Does it make sense to create a little utility function?


// Check that interpretation strings are not empty. The actual interpretation
// values are target-specific and not validated here.
Check(!ResultInterp.empty(),
"result interpretation metadata string must not be empty", Call);
Check(!InputInterp.empty(),
"input interpretation metadata string must not be empty", Call);

if (RoundingInterp != "none") {
std::optional<RoundingMode> RM =
convertStrToRoundingMode(RoundingInterp);
Check(RM && *RM != RoundingMode::Dynamic,
"unsupported rounding mode argument", Call);
}

// Check saturation parameter (must be 0 or 1)
auto *SaturationOp = dyn_cast<ConstantInt>(Call.getArgOperand(4));
Check(SaturationOp, "saturation operand must be a constant integer", Call);
if (SaturationOp) {
uint64_t SatVal = SaturationOp->getZExtValue();
Check(SatVal == 0 || SatVal == 1,
"saturation operand must be 0 or 1", Call);
}
break;
}
#define BEGIN_REGISTER_VP_INTRINSIC(VPID, ...) case Intrinsic::VPID:
#include "llvm/IR/VPIntrinsics.def"
#undef BEGIN_REGISTER_VP_INTRINSIC
Expand Down
1 change: 1 addition & 0 deletions llvm/lib/Target/SPIRV/SPIRVCommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ static const std::map<std::string, SPIRV::Extension::Extension, std::less<>>
{"SPV_INTEL_tensor_float32_conversion",
SPIRV::Extension::Extension::SPV_INTEL_tensor_float32_conversion},
{"SPV_KHR_bfloat16", SPIRV::Extension::Extension::SPV_KHR_bfloat16},
{"SPV_EXT_float8", SPIRV::Extension::Extension::SPV_EXT_float8},
{"SPV_EXT_relaxed_printf_string_address_space",
SPIRV::Extension::Extension::
SPV_EXT_relaxed_printf_string_address_space},
Expand Down
37 changes: 37 additions & 0 deletions llvm/lib/Target/SPIRV/SPIRVGlobalRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,43 @@ SPIRVGlobalRegistry::getOpTypeFloat(uint32_t Width,
});
}

SPIRVType *SPIRVGlobalRegistry::getOrCreateOpTypeFloatWithEncoding(
uint32_t Width, MachineIRBuilder &MIRBuilder,
SPIRV::FPEncoding::FPEncoding FPEncode) {
auto Key = std::make_pair(Width, static_cast<unsigned>(FPEncode));
if (SPIRVType *Existing = FloatTypesWithEncoding.lookup(Key)) {
// Check if the existing type is from the current function
const MachineFunction *TypeMF = Existing->getParent()->getParent();
if (TypeMF == &MIRBuilder.getMF())
return Existing;
// Type is from a different function, need to create a new one for current function
}

SPIRVType *SpvType = getOpTypeFloat(Width, MIRBuilder, FPEncode);
LLVMContext &Ctx = MIRBuilder.getMF().getFunction().getContext();
Type *LLVMTy = nullptr;
switch (Width) {
case 8:
LLVMTy = Type::getInt8Ty(Ctx);
break;
case 16:
LLVMTy = Type::getHalfTy(Ctx);
break;
case 32:
LLVMTy = Type::getFloatTy(Ctx);
break;
case 64:
LLVMTy = Type::getDoubleTy(Ctx);
break;
default:
report_fatal_error("unsupported floating-point width for SPIR-V encoding");
}

SpvType = finishCreatingSPIRVType(LLVMTy, SpvType);
FloatTypesWithEncoding.try_emplace(Key, SpvType);
return SpvType;
}

SPIRVType *SPIRVGlobalRegistry::getOpTypeVoid(MachineIRBuilder &MIRBuilder) {
return createOpType(MIRBuilder, [&](MachineIRBuilder &MIRBuilder) {
return MIRBuilder.buildInstr(SPIRV::OpTypeVoid)
Expand Down
7 changes: 7 additions & 0 deletions llvm/lib/Target/SPIRV/SPIRVGlobalRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class SPIRVGlobalRegistry : public SPIRVIRMapping {

DenseMap<SPIRVType *, const Type *> SPIRVToLLVMType;

DenseMap<std::pair<unsigned, unsigned>, SPIRVType *>
FloatTypesWithEncoding;

// map a Function to its definition (as a machine instruction operand)
DenseMap<const Function *, const MachineOperand *> FunctionToInstr;
DenseMap<const MachineInstr *, const Function *> FunctionToInstrRev;
Expand Down Expand Up @@ -413,6 +416,10 @@ class SPIRVGlobalRegistry : public SPIRVIRMapping {
// Return the number of bits SPIR-V pointers and size_t variables require.
unsigned getPointerSize() const { return PointerSize; }

SPIRVType *getOrCreateOpTypeFloatWithEncoding(
uint32_t Width, MachineIRBuilder &MIRBuilder,
SPIRV::FPEncoding::FPEncoding FPEncode);

// Returns true if two types are defined and are compatible in a sense of
// OpBitcast instruction
bool isBitcastCompatible(const SPIRVType *Type1,
Expand Down
Loading
Loading