DO NOT MERGE: changes over google/main for Belfort's pipeline - #3349
Draft
AlexanderViand wants to merge 18 commits into
Draft
DO NOT MERGE: changes over google/main for Belfort's pipeline#3349AlexanderViand wants to merge 18 commits into
AlexanderViand wants to merge 18 commits into
Conversation
AlexanderViand
force-pushed
the
alex/close-to-main
branch
3 times, most recently
from
August 20, 2026 08:52
5d8a3bf to
9e6d372
Compare
AlexanderViand
force-pushed
the
alex/close-to-main
branch
6 times, most recently
from
August 21, 2026 07:07
ae63b56 to
fcc324f
Compare
AlexanderViand
force-pushed
the
alex/close-to-main
branch
13 times, most recently
from
August 27, 2026 06:43
7e90a60 to
afa7c28
Compare
This change: - Implements the LWE-to-Lattigo lowering pattern for kernel.linear_transform to lattigo.ckks.linear_transform. - Enables has_kernel_linear_transform = 1 in Lattigo target config. - Fixes the Lattigo emitter to dynamically convert float32 diagonals to float64 in Go. - Resolves LevelQ and Scale matching issues in the emitter using dynamic ciphertext level instead of compile-time constants. - Aligns encoding slots with actual slot counts for CKKS bootstrapping. After this change, the e2e lattigo tests using linalg.matvec lower to use the linear_transform op. Still TODO is lowering a conv operator to an intermediate matvec op so it can be preserved as a linear_transform op, which is a bit of a larger change because it requires extracting the lowering for conv ops to occur before layout-propagation, and hard-coding the layout choice. PiperOrigin-RevId: 960437546
Squashed from edc9c4e, 5fbcbb6 and 9694862 on the belfort fork. - sky_x86_64_wheel.yaml / sky_arm64_wheel.yaml build the belfort-heir wheels with cibuildwheel on a SkyPilot instance. - sky/warm_cache.yaml populates the BuildBuddy remote cache for x86_64 linux by building //tools:all with the exact config setup.py uses, so a consumer's `uv sync` hits the cache instead of rebuilding LLVM. - .skyignore keeps bazel output trees out of the workdir sync. - .bazelrc / bazel/download_config.cfg / setup.py carry the matching remote-cache and pocketfft download settings. Fork-only infrastructure: not upstreamable, and required for medusa to be able to pin this fork as an installable package. The warm-cache setup installs no packages: the build registers a hermetic LLVM toolchain, so bazel supplies its own compiler and linker, and wget is already on the image. The apt fetch it replaced downloaded ~200 MB of clang to be ignored, and was the slowest and least reliable step of a launch.
Squashed from f587d36, 67261d3 and 1b2a2c9 on the belfort fork. - Rename the published distribution heir_py -> belfort-heir (pyproject.toml, frontend/heir/_extras.py) so medusa can depend on the fork without colliding with upstream's package name. - scripts/get_version.py: stop hard-coding the heir_py distribution name and derive it, with the matching test update. - Publish from the ml-pipeline branch rather than main (release workflow). - Pass GitHub context and job outputs to release scripts through step environment variables, avoiding shell template injection. Fork-only packaging: not upstreamable, and required for medusa to pin this fork.
* Mirror Lattigo's FindBestBSGSRatio exactly for sparse diagonal sets
findBestBSGSRatio guarded the ratio comparison with nbN1 > 0 and compared in
integers. Lattigo divides in floating point without that guard, so a sparse
diagonal set, where nbN1 reaches 0 with nbN2 > 0, must take the > maxRatio
branch on the resulting +inf (and continue on a NaN 0/0). Guarding skips those
iterations and can select a different baby-step size than the backend, which
then generates rotation keys for the wrong Galois elements. Dense diagonal sets
never reach nbN1 == 0, so this only shows up for sparse transforms such as an
expanded convolution filter.
* Encode plaintexts at the requested slot count
Encoding at the ring's full slot count makes every ciphertext full-width, which
silently changes what a rotation means: rotations are modulo the slot count, so
a matrix packed at the requested (smaller) width lands in the wrong slots. On a
convolutional model that shows up as a ~5-orders-of-magnitude accuracy loss
(2.9e-02 relative error against 6.2e-07) rather than a failure.
The requested slot count is what the layouts packed to, so encode to that and
leave the ring as large as the parameters need it. The diagonals of a linear
transform are then narrower than the ring, which the verifier now allows: the
transform acts on the leading slots.
* Record the packing width before the bootstrap bump
Lattigo bootstrapping needs LogN >= 14, and since the ring dimension follows
from the slot count, the pass bumps minSlotCount to 8192 to get there. That
bump is about the ring, but it was also written back as the requested slot
count, which is what the layouts packed to and what plaintexts are encoded at.
Widening it desyncs the packed layouts from the ciphertexts.
Keep the pre-bump value for the requested-slot-count attribute.
* Require a linear operand for the kernel ops when placing relinearization
A linear transform is a bundle of rotations under Galois keys, and a Chebyshev
evaluation multiplies its input, so both need a linear ciphertext just as
tensor_ext.rotate does. Without that constraint the model may defer a
relinearization past them, leaving it applied to an already-linear result:
lattigo then fails with "ctIn.Degree() should be 2 but is 1".
This only becomes reachable once linear transforms survive to the backend.
* Only bootstrap ciphertexts
The bootstrap target walk filtered values by secret type, which does not
exclude a plaintext: a plaintext has no level for the analysis to track, that
reads as "needs bootstrap", and the pass then put a mgmt.bootstrap on an
mgmt.init. Nothing lowers that, and it surfaces far away as a ct-ct multiply
against a plaintext operand ('ckks.mul' op operand #0 must be
lwe-ciphertext-like).
Ask the secretness analysis as well.
* Charge lattigo's bootstrap no compute levels
Lattigo runs its bootstrapping circuit in its own parameter set, whose ring
carries extra primes: bootstrapping.NewParametersFromLiteral takes the compute
parameters as the *residual* ones. A bootstrap therefore returns the ciphertext
at the top of the compute chain and consumes none of it. Probing the emitted Go
for tcn confirms it: Bootstrap(in=41) returns out=26 with maxlevel=26.
Charging 16 levels made every level after a bootstrap 16 too low, which both
inflated the modulus chain and mis-scheduled the rescales that follow.
The budget the pass works from is levelBudget + bootstrapLevelsConsumed, so
a test that stated its budget net of the old 16-level charge now has to name
the total it meant: if.mlir asks for 18 where it used to ask for 2.
relu_composite now also inserts a real bootstrap, so its e2e test accepts
the bootstrapping evaluator and covers the correspondingly slower path.
loop_invariant_hoist_reproducer instead keeps the old charge as an explicit
override: its module was captured off tcresnet8small under a 16-level
charge, and reaching the loop with the levels exhausted is a precondition of
the hoisting it asserts rather than a property of lattigo.
* Bootstrap a copy so the operand survives
lattigo's Bootstrap mod-ups its argument into the bootstrapping ring in place
and leaves it there, which a probe on the emitted Go shows plainly:
boot BEFORE: ct4 lvl=1 log2scale=45
boot AFTER : ct4 lvl=29 log2scale=52 <- argument clobbered
out ct92 lvl=14 log2scale=45 <- result is fine
The op is a function of its operand, though, so the operand may still be live,
and a composite ReLU makes that the common case: x * step(x) needs the
un-bootstrapped x alongside the refreshed branch that produced step(x). On tcn,
7 of 28 bootstrap operands are read again afterwards - already at the CKKS
level, before any in-place assignment - so those reads take a ciphertext that
is no longer in the compute ring. The values are wrong from there on, and the
scale it leaves behind (2^52 against a base of 2^45) propagates until a later
bootstrap fails lattigo's message-ratio check:
panic: cannot BootstrapMany: initial Q/Scale = 7.999996
< 0.5*Q[0]/MessageRatio = 256.000000
Hand lattigo a copy. A ciphertext copy is negligible next to a bootstrap, so
this is unconditional rather than guarded on the operand having later uses.
This is the convention the dialect already uses for a value-semantics op whose
lattigo call is in-place: rescale_new, drop_level_new and negate_new all copy
the input and hand the copy to the in-place method. bootstrap was the one that
passed its operand straight in. Keeping the op Pure is right - the copy is what
makes it true - and it lets CSE deduplicate bootstraps of the same value, which
is worth having for the most expensive op in the program.
The op description still says it "applies bootstrapping in-place", which now
describes the lattigo call rather than the op; worth a separate docs pass.
* Model the level a lattigo chebyshev consumes
In the lattigo dialect every ciphertext has the same opaque type, so an
analysis learns a value's level only from the interfaces an op declares.
ckks.chebyshev declared none and was therefore modelled as consuming nothing,
while it consumes one level per level of the binary evaluation tree. Nothing
checks that belief, so the error is silent: alloc-to-inplace hands a
chebyshev's exhausted buffer to a later op as its destination, the backend
evaluates into it at min(operands, destination), and the quietly truncated
result surfaces much later as "cannot Rescale: input Ciphertext level is too
low". Declare ReducesLevelOpInterface, mirroring the depth
kernel.eval_chebyshev already models.
ckks.linear_transform deliberately does NOT declare it, and the op
description now says why: the transform is built at the input ciphertext's
own level, so evaluating it consumes no level, and the rescale the lowering
emits straight afterwards is the single level the multiplication costs.
Charging the transform as well double-counts, which drifts every value
downstream of a transform one level too deep until the next bootstrap resets
it, and makes the alloc-to-inplace level comparison unsound in a way the pass
cannot see.
* Do not assert in getRotationIndices on a bufferized diagonals operand
CKKSLinearTransformOp::getRotationIndices cast the diagonals operand to
RankedTensorType. The operand is a tensor before bufferization and a memref
after it, and the accessor is reachable in both states, so on an
already-bufferized module the cast aborts:
Casting.h:560: cast<To = mlir::RankedTensorType, From = mlir::ShapedType>:
Assertion `isa<To>(Val)' failed
The full pipeline runs configure-crypto-context before bufferization, so this
does not fire during a normal compile. It fires when the analysis is run on a
dumped module, which is how rotation-key collection gets debugged. Match on
ShapedType, which is all the accessor needs.
A ciphertext argument does not have to start at the top of the modulus chain. HEIR asks the client to encrypt at the level the computation actually begins at, which for a model whose first op is a linear transform is well below the top. On tcn the entry ciphertext is fresh but sits at level 2 of an eleven-modulus chain: func.func @tcn(%0: !lwe.lwe_ciphertext<..., modulus_chain = <elements = <11 moduli>, current = 2>>, ...) The lattigo lowering then gives every ciphertext the same opaque !lattigo.rlwe.ciphertext, and LevelAnalysis::setToEntryState joins every entry lattice to LevelState(0) -- the top of the chain. Everything derived from that argument is believed eight levels shallower than it is. Nothing verifies that belief. lattigo-alloc-to-inplace is the only pass that runs LevelAnalysis after this lowering, and it compares the believed level of a candidate buffer against the believed level of an op's result to decide whether reuse is safe, so a wrong belief costs reuse it should have taken or takes reuse it should have refused. On tcn this changes 34 reuse decisions. Record the starting level while the LWE type still carries it, and have setToEntryState read it back. `current` indexes the module's Q chain, so the depth is measured against the length of that chain from the scheme parameters rather than against the type's own element list: a lowered LWE type can carry a truncated view of the chain -- a fully consumed value in the same function arrives as `elements = <1 modulus>, current = 0` -- and measuring against a truncated list silently reports depth 0. Modules without scheme parameters (hand-written test IR) still fall back to the type's own list. Note this restores information the lowering drops; it is not what keeps the backend from truncating a result. Lattigo resolves an in-place result to min(operand levels, receiver level), so soundness there comes from the emitter giving the receiver the operand's level, not from the level analysis being right.
RotationAnalysis rebuilds an enclosing scf.for as an ArithmeticDag and enumerates the shifts its rotations use, so configure-crypto-context can generate exactly the Galois keys a program needs. DagBuilder covered scf.for, scf.if and the tensor ops, but not an affine.for nested in that reconstructed loop. An unhandled nested op is resolved as an opaque variable, severing everything upstream of it from the DAG root. The analysis still reports success, but the rotations inside the affine loop disappear. On tcn, rotate(ct, %i + 64) over %i = 1..5 therefore omitted the key for rotation 65 and failed at runtime. Teach DagBuilder to model constant-bound affine.for and affine.yield just like their scf counterparts. The lit test exercises both the induction variable and induction-variable-plus-constant forms inside an affine.for nested in scf.for; the unit test covers DAG evaluation directly. This does not handle a rotation whose enclosing loop is a bare affine.for. RotationAnalysis still rejects that case under TODO(google#2712); this change only prevents nested affine loops from severing an scf.for DAG.
The storage type's element list was built by inserting into a DenseSet<Type> and then copying the set out. Type is a pointer wrapper, so the set hands its elements back in the order of their addresses, and that order is not stable from one run to the next. The order is not cosmetic: the element list is part of the __preprocessing function's signature and fixes which memref each store and load site indexes, so permuting it produces a different module for the same input. A storage with one element type could not show this; a storage holding several -- plaintexts next to prepared linear transformations -- can. Collect the types in first-occurrence order instead, which is what the nearby uniqueElementTypes() helper already documents as the intended contract.
SecretnessAnalysis runs before walk-based rewriting. Replacing an earlier eval creates a new operand with no lattice entry, while each later eval result remains analyzed. Query the result so chained secret evaluations consistently use native lowering.
Add a ConvertToEmitC dialect interface that emits scalar math.sqrt as std::sqrt and includes <cmath>. This keeps Math operations out of the C++ translator until equivalent support can be upstreamed to MLIR.
Two problems, one shape. A Halevi-Shoup convolution or matvec was materialised as an explicit rotate / multiply / accumulate DAG, one branch per diagonal, and the lattigo backend then built and encoded a lintrans.Transformation inside the hot function, so every inference re-encoded every transform. For a backend that can evaluate a linear transform directly the DAG is the wrong shape twice over: it hides from the backend that the whole thing is one transform, so BSGS and hoisted rotations cannot be used, and it inflates the IR by the number of diagonals. On the pooling test it is 65 rotations where one op will do, and under rolled kernels it is an scf.for/scf.if rotation nest that need not exist at all. Keep the transform compact, then split preparing it from applying it: * Emit the transform as a single tensor_ext.rotate_and_reduce carrying the diagonals, marked as a linear transform so implement-rotate-and-reduce leaves it alone, and lower that to kernel.linear_transform so the abstraction survives into the backend rather than being expanded on the way. Zero diagonals of a mostly-zero matrix are dropped and their offsets recorded, so the backend neither encodes nor rotates for them. A squat packing (rows < cols) still needs the partial rotate-and-reduce afterwards, mirroring implementHaleviShoup. The convolution paths and the matvec kernel both take this route -- every fully-connected layer that torch lowers as matmul+transpose reaches that kernel as a linalg.matvec after linalg preprocessing -- gated on the target advertising has_kernel_linear_transform, so a backend without one still gets the expanded DAG it can handle. * Take the diagonals as an operand rather than an attribute. Row k is the generalized diagonal named by diagonal_indices[k]; when source_row_indices is present, that diagonal is read from source_row_indices[k] instead. The packed matrix can then be a resource or a preprocessed value instead of inline IR, which keeps it out of the emitted source. For a constant producer, the lowering folds selection into a compact constant. For any other producer it retains the full tensor and carries the selected source rows through preparation and backend emission, avoiding a large extract/concat gather in preprocessing. Inferring either representation from the row count would be ambiguous and would oblige every backend to guess which contract it received. * Split kernel.linear_transform into kernel.prepare_linear_transform, which encodes the diagonals into an opaque !kernel.prepared_linear_transform, and kernel.apply_linear_transform, which evaluates it on a ciphertext. The prepare op implements PlaintextEncodeOpInterface and its result type PlaintextTypeInterface, so the existing generic split-preprocessing pass hoists the whole preparation into the __preprocessing helper with no new backend machinery, for every backend at once. Once preparation is offline, imperfect transforms (row selection, zero-padding, uneven embeddings) cost setup time rather than evaluation time. The prepared type carries the static parameters the preparation bakes in: level, slot count, and BSGS split. Two apply sites with different levels then need two distinct prepares by construction, and the apply verifier rejects a level mismatch that backends today only discover as a silently mis-scaled result, since they read the level off the ciphertext at runtime. (A transform inside a rolled loop runs at exactly one level, because scf.for iter_args fix the loop-carried ciphertext type, so one prepared transform per op always suffices.) The pass splits every kernel.linear_transform whose input type determines the parameters, gated on the target declaring has_prepared_linear_transform and on the module being CKKS -- only the CKKS lowerings implement prepare/apply, so splitting anything else would leave ops no backend pattern can convert. kernel.linear_transform stays valid sugar for backends that lower it directly. * Lower a prepared transform to lattigo and emit it: new ckks operations over a prepared-transformation type map onto NewTransformation plus Encode, which split-preprocessing hoists into __preprocessing, leaving __preprocessed with the evaluator call alone. The rotations are keyed off the slot count the prepare op records, since lattigo reduces its BSGS split and Galois elements modulo that count, so deriving them from the width of the diagonals operand can omit a required key. `getEncodedSlotCount` centralizes this choice for both direct CKKS lowering and prepared transforms, extending the requested slot-count handling to both paths. The level comes from lwe::getLevel: both the preparation and the direct lowering computed the consumed depth (chain size - 1 - current) and handed it to lattigo as levelQ, which is the number of remaining moduli, i.e. exactly chain.getCurrent(). The transform's bsgs_ratio is honoured, of which the prepared type records the log2, rather than silently forced to the backend default. Optional source-row indices are forwarded to the emitter so it encodes the selected full-width rows directly. Applying the prepared transform preserves the ciphertext level; the explicit rescale performs the only level drop, so backend level analysis does not double-count it and reuse storage from a physically lower level. * Lower a multi-type preprocessing storage to one memref per type. The lattigo preprocessing lowering assumed a storage with exactly one unique element type (rlwe plaintexts) and mapped it onto a single flat memref; a hoisted prepared transform adds a second. Reuse the generic one-memref-per-unique-type machinery from preprocessing-to-memref, moved into the shared conversion util, so __preprocessing returns one slice per element type. Backend lowerings can collapse several storage element types into one (every prepared-transform type becomes the one lattigo transformation type), so the converted element list is deduplicated in first-occurrence order, and the store/load index lookup matches that order. The conversion tests move with it: pooling and conv2d_chain asserted the expanded form (including pooling's rolled-kernel prefix), and linear_transform checked the diagonals as an attribute. MNIST no longer generates or passes its two encrypted-zero accumulator helpers because the compact transform does not materialize those explicit accumulators. implement-rotate-and-reduce declares the kernel dialect as a dependent dialect, since it now creates kernel.linear_transform ops. Co-authored-by: Marc Desgroseilliers <marc.desgroseilliers@belfortlabs.com>
CKKS ciphertext multiplication needs a modulus limb available for its following rescale. Run a conservative headroom analysis before ordinary waterline placement and bootstrap operands whose downstream multiplication would otherwise reach an exhausted level. The repair treats the refreshed value as the new SSA version: a bootstrap inserted at a safe point replaces every dominated use of the operand, not only the multiplication that exposed the shortage. This deliberately broad repair prevents later uses from continuing with the exhausted value, but can refresh uses that do not themselves need it; the subsequent scale-aware cleanup removes redundant detours where the original value already had the required state. Validate populated scales in the default RLWE pipeline so an impossible CKKS schedule is rejected by the compiler rather than reaching a backend.
The conservative headroom repair replaces all dominated uses with a refreshed value. After scale placement, this can leave an adjust-scale, mod-reduce or level-reduce chain followed by a bootstrap even though an ancestor already has the exact level, scale and ciphertext dimension the use requires. Mark only headroom-repair bootstraps with the discardable mgmt.mul_headroom attribute. Once PopulateScaleCKKS has annotated exact management state, walk backward through those management-only operations and replace a marked bootstrap with an ancestor carrying the same MgmtAttr. Remove the marker from bootstraps that remain so it cannot escape the pipeline. The regression now keeps the bootstrap required by the square branch while asserting that no redundant bootstrap or marker remains on the other branch.
Assign explicit roles to the logical entry point and its encryption, decryption, preprocessing, and evaluation helpers. Preserve the original cleartext input and result types so backend interfaces can expose a stable API without reconstructing it from lowered function signatures. Distinguish server preprocessing from client packing, and update helper classification and split Lattigo emission to use the new role while retaining compatibility with existing packing helpers.
* lattigo: tag the generated configure function with its interface role
Every other client/server helper carries a role attribute naming the entry it
belongs to; <entry>__configure carried none, leaving it discoverable only by
its name suffix. Lattigo fuses context setup and key generation into that one
function, so it takes client.setup_func alone.
* lwe: index the encrypted-zero client helpers
client.enc_zero_func and client.enc_zero_arg were bare unit attributes, so
pairing a helper with the entry argument it feeds meant relying on the order
the arguments were appended in. Give both the {func_name, index} dictionary
every other indexed helper role uses; the index is already computed to name
the function.
* client interface: leave existing helpers out of the entry roles
The pass ran over every function in the module, so the layout-assignment
helper convert-to-ciphertext-semantics outlines earlier also picked up
heir.entry_func, heir.entry_input_types and server.evaluate_func, all naming
itself as the entry. Skip functions that already carry a helper role, as
split-preprocessing does.
* split-preprocessing: record the forwarded entry arguments
The preprocessing function takes a subset of the entry's arguments, chosen by
dataflow and ordered by walk order. That mapping was only recoverable from the
operand list of the call in the combined entry function, so a backend building
a public interface had to re-derive it. Record it as entry_arg_indices in the
server.preprocessing_func role.
* entry interface: share the role discovery
Collecting an entry point's client/server roles reads nothing but the
attributes in ModuleAttributes.h, so it does not belong inside the Cheddar
EmitC pass. Move it to lib/Utils, anchored on the backend-independent entry
contract rather than on the setup function, and let a backend that fuses
key generation into setup, or that has no client encryption helpers at all,
still find what is there. Encrypted-zero helpers are collected too.
* lattigo: emit a Go entry interface
A harness written against the generated Lattigo ABI has to know whether the
program bootstraps (it changes __configure's arity), how many encrypted zeros
the client supplies, how many storage slices __preprocessing returns, and
which cleartext arguments it is given -- none of which is visible in Go
without reading the emitted file.
Add --emit-lattigo-interface, which reads the entry roles and emits a facade
whose shape is fixed: a context struct holding what __configure returns, and
Setup/Encrypt/Preprocess/Evaluate/Decrypt taking and returning [][]float64.
The Cheddar backend has had the C++ equivalent since it generates its own
header; this is the Go counterpart, and it additionally covers the encrypted
zeros. Roles a module does not have yield no method, so a program entered at
ciphertext semantics still gets the context and the server side.
* lattigo: exercise the entry interface in the preprocessing example
heir_lattigo_lib gains an opt-in entry_interface flag, and matvec turns it on:
its test now runs the same computation a third time through the facade and
gets the same values as the hand-written calls. Preprocessing helpers move to
a separate package under the split, so the facade qualifies those calls the
way the main emitter does.
* lattigo: let the entry interface take a fixed prefix
The interface's types and functions are named after the entry function, so a
harness has to be generated per model to name them. --interface-prefix pins
them to a chosen name instead, which is what a static harness needs.
* lattigo: let a harness supply its own ciphertexts to the interface
A program entered at ciphertext semantics has no encryption helpers, so the
interface cannot build its encrypted inputs. EncryptedFrom takes the
ciphertexts the harness made itself, fills the cleartext arguments alongside
them, and still calls the generated helpers for the encrypted zeros.
* lattigo: emit the server side without an entry contract
A program entered at ciphertext semantics never ran --add-client-interface, so
it has no heir.entry_func to anchor on and no logical input types. Fall back to
the setup role for the entry's name and to the evaluate function for its
argument list, and emit what such a module does have: the context, the
preprocessing and evaluation, and EncryptedFrom. Preprocess is emitted whether
or not the program was split, so a harness calls the same sequence either way.
Make preprocessing.load_resource a destination-style operation so standard bufferization owns its allocation and aliasing instead of treating a loaded resource as storage materialized by a custom result-producing bufferization model. With tensor semantics the op returns a result tied to its destination, remains effect-free and is conditionally speculatable. With buffer semantics it writes the destination and has no result, so the old unconditional Pure trait was not truthful. Mark the loaded tensor as non-writable during bufferization; a later tensor write receives separate storage and a copy rather than mutating the resource. Externalize-constants now supplies tensor.empty as the destination, and One-Shot Bufferize lowers that to an ordinary allocation followed by the memref-form load. The Lattigo, OpenFHE and TFHE Rust emitters recognize that destination, suppress the ordinary allocation, and retain their static resource storage. This keeps runtime storage policy in the backends while giving the IR and bufferization analysis ordinary destination-style semantics.
AlexanderViand
force-pushed
the
alex/close-to-main
branch
from
August 27, 2026 16:54
afa7c28 to
ea6cf8d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Creating this simply as a nice place to see the stack and its diff, not intended for review/merge!
This branch tracks the changes we need to make Belfort's pipeline work with the
google/mainversion of HEIR.I've opened individual PRs for pretty much all of this (except the experimental or fork-specific parts) and tagged them below.
Commits
math.sqrtin EmitC (No HEIR PR, since this should probably be done upstream?)