Skip to content

OpenFhePkeEmitter's tensor.insert emission aliases SSA result to destination buffer, collapsing distinct outputs in multi-result functions #3196

Description

@VedantParanjape

printOperation(tensor::InsertOp) at
lib/Target/OpenFhePke/OpenFhePkeEmitter.cpp:1708-1729 unconditionally
mutates the destination in place and calls
variableNames->mapValueNameToValue(op.getResult(), op.getDest()),
aliasing the SSA result's C++ variable name to the destination buffer.
This alias is unsafe when the destination has more than one use, for
example when two sequential tensor.insert %ct into %buf[c0] operations
both consume the same buffer and produce two SSA values that are
independently consumed downstream. Both SSA values then print as the same
C++ variable, and any consumer of the first one silently gets the mutated
(second) value.

The sibling printOperation(tensor::InsertSliceOp) at lines 1565-1581
already does the correct thing: it allocates a fresh std::vector<T> vResult(vDest);
when the result and dest names differ, then writes into
the copy. The scalar InsertOp should follow the same pattern.

We hit this in a multi-result kernel where the --scheme-to-openfhe
conversion wraps each scalar !ct result into a tensor<1x!ct> via
tensor.insert, and both wraps share the same tensor.empty buffer.
The emitted C++ becomes:

std::vector<CiphertextT> v51(1);
v51[0] = ct133;                           // %wrap1 aliased to v51
v51[0] = ct142;                           // %wrap2 also aliased to v51
return {v51, v51};                        // both fields = last-written v51

Combined with the shared_ptr semantics of Ciphertext<DCRTPoly>, both
multi-result fields carry the same ciphertext at runtime.

Suggested fix (drop-in from the InsertSliceOp pattern): in
printOperation(tensor::InsertOp), when getNameForValue(op.getResult()) != getNameForValue(op.getDest()), emit
std::vector<T> vResult(vDest); vResult[idx] = scalar; and do NOT call
mapValueNameToValue.

Minimal reproducer (also usable as a regression test):

// RUN: heir-translate --emit-openfhe-pke %s | FileCheck %s
!cc = !openfhe.crypto_context
!ct = !openfhe.ciphertext
// CHECK-LABEL: mm_bug_two_wraps
// CHECK: std::vector<{{.*}}> [[V1:v[0-9]+]](
// CHECK: [[V1]][0] = %arg1
// CHECK: std::vector<{{.*}}> [[V2:v[0-9]+]](
// CHECK: [[V2]][0] = %arg2
// CHECK: return {[[V1]], [[V2]]};
func.func @mm_bug_two_wraps(%arg0: !ct, %arg1: !ct, %arg2: !ct)
    -> (tensor<1x!ct>, tensor<1x!ct>) {
  %c0 = arith.constant 0 : index
  %buf = tensor.empty() : tensor<1x!ct>
  %wrap1 = tensor.insert %arg1 into %buf[%c0] : tensor<1x!ct>
  %wrap2 = tensor.insert %arg2 into %buf[%c0] : tensor<1x!ct>
  return %wrap1, %wrap2 : tensor<1x!ct>, tensor<1x!ct>
}

Likely also affects the Lattigo emitter (analogous
printOperation(tensor::InsertOp) at lib/Target/Lattigo/) — should be
checked and patched symmetrically.

Affected files:

  • lib/Target/OpenFhePke/OpenFhePkeEmitter.cpp:1708-1729 (root cause)

PS: Triaged using claude opus

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions