Enzyme main
Loading...
Searching...
No Matches
HMCUtils.cpp
Go to the documentation of this file.
1//===- HMCUtils.cpp - Utilities for HMC/NUTS inference -------* C++ -*-===//
2//
3// This file implements utility functions for Hamiltonian Monte Carlo (HMC) and
4// No-U-Turn Sampler (NUTS) implementations.
5//
6// Reference:
7// https://github.com/pyro-ppl/numpyro/blob/master/numpyro/infer/hmc_util.py
8//
9//===----------------------------------------------------------------------===//
10
11#include "HMCUtils.h"
12
14#include "Dialect/Ops.h"
15#include "mlir/Dialect/Arith/IR/Arith.h"
16#include "mlir/Dialect/Func/IR/FuncOps.h"
17#include "mlir/Dialect/Math/IR/Math.h"
18
19#include <cmath>
20#include <limits>
21
22using namespace mlir;
23using namespace mlir::enzyme;
24using namespace mlir::impulse;
25
34
36 assert(values.size() == 18 && "Expected 18 NUTSTreeState fields");
37 return {.q_left = values[0],
38 .p_left = values[1],
39 .grad_left = values[2],
40 .q_right = values[3],
41 .p_right = values[4],
42 .grad_right = values[5],
43 .q_proposal = values[6],
44 .grad_proposal = values[7],
45 .U_proposal = values[8],
46 .H_proposal = values[9],
47 .depth = values[10],
48 .weight = values[11],
49 .turning = values[12],
50 .diverging = values[13],
51 .sum_accept_probs = values[14],
52 .num_proposals = values[15],
53 .p_sum = values[16],
54 .rng = values[17]};
55}
56
57SmallVector<Type> NUTSTreeState::getTypes() const {
58 SmallVector<Type> types;
59 for (auto val : toValues())
60 types.push_back(val.getType());
61 return types;
62}
63
64Value impulse::conditionalDump(OpBuilder &builder, Location loc, Value value,
65 StringRef label, bool debugDump) {
66 if (debugDump) {
67 return enzyme::DumpOp::create(builder, loc, value.getType(), value,
68 builder.getStringAttr(label))
69 .getOutput();
70 }
71 return value;
72}
73
74/// Creates a 2D identity matrix of the specified type.
75static Value createIdentityMatrix(OpBuilder &builder, Location loc,
76 RankedTensorType matrixType) {
77 assert(matrixType.getRank() == 2 && "Expected 2D tensor type");
78 assert(matrixType.getShape()[0] == matrixType.getShape()[1] &&
79 "Expected square matrix type");
80
81 int64_t size = matrixType.getShape()[0];
82 auto elemType = matrixType.getElementType();
83
84 SmallVector<Attribute> values;
85 values.reserve(size * size);
86 for (int64_t i = 0; i < size; ++i) {
87 for (int64_t j = 0; j < size; ++j) {
88 double val = (i == j) ? 1.0 : 0.0;
89 values.push_back(builder.getFloatAttr(elemType, val));
90 }
91 }
92
93 auto denseAttr = DenseElementsAttr::get(matrixType, values);
94 return arith::ConstantOp::create(builder, loc, matrixType, denseAttr);
95}
96
97/// Creates a permutation matrix of size n x n.
98static Value createPermutationMatrix(OpBuilder &builder, Location loc,
99 RankedTensorType matrixType) {
100 assert(matrixType.getRank() == 2 && "Expected 2D tensor type");
101 assert(matrixType.getShape()[0] == matrixType.getShape()[1] &&
102 "Expected square matrix type");
103
104 int64_t size = matrixType.getShape()[0];
105 auto elemType = matrixType.getElementType();
106
107 SmallVector<Attribute> values;
108 values.reserve(size * size);
109 for (int64_t i = 0; i < size; ++i) {
110 for (int64_t j = 0; j < size; ++j) {
111 double val = (j == size - 1 - i) ? 1.0 : 0.0;
112 values.push_back(builder.getFloatAttr(elemType, val));
113 }
114 }
115
116 auto denseAttr = DenseElementsAttr::get(matrixType, values);
117 return arith::ConstantOp::create(builder, loc, matrixType, denseAttr);
118}
119
120/// Computes A[::-1, ::-1] using permutation matrix through P @ A @ P.
121static Value reverseRowsAndColumns(OpBuilder &builder, Location loc,
122 Value matrix) {
123 auto matrixType = cast<RankedTensorType>(matrix.getType());
124 auto P = createPermutationMatrix(builder, loc, matrixType);
125
126 // PA = P @ A
127 auto PA = impulse::DotOp::create(
128 builder, loc, matrixType, P, matrix, builder.getDenseI64ArrayAttr({}),
129 builder.getDenseI64ArrayAttr({}), builder.getDenseI64ArrayAttr({1}),
130 builder.getDenseI64ArrayAttr({0}));
131
132 // PAP = PA @ P
133 return impulse::DotOp::create(
134 builder, loc, matrixType, PA, P, builder.getDenseI64ArrayAttr({}),
135 builder.getDenseI64ArrayAttr({}), builder.getDenseI64ArrayAttr({1}),
136 builder.getDenseI64ArrayAttr({0}));
137}
138
139Value impulse::applyInverseMassMatrix(OpBuilder &builder, Location loc,
140 Value invMass, Value momentum,
141 RankedTensorType positionType) {
142 if (!invMass) {
143 return momentum;
144 }
145
146 auto invMassType = cast<RankedTensorType>(invMass.getType());
147
148 if (invMassType.getRank() == 1) {
149 // Diagonal: element-wise
150 auto diagMass =
151 impulse::ReshapeOp::create(builder, loc, positionType, invMass);
152 return arith::MulFOp::create(builder, loc, diagMass, momentum);
153 } else if (invMassType.getRank() == 2) {
154 // Dense: v = invMass @ p
155 return impulse::DotOp::create(
156 builder, loc, positionType, momentum, invMass,
157 builder.getDenseI64ArrayAttr({}), builder.getDenseI64ArrayAttr({}),
158 builder.getDenseI64ArrayAttr({1}), builder.getDenseI64ArrayAttr({0}));
159 }
160
161 emitError(loc, "ProbProg: Provided invMass must have rank 1 or 2, got rank " +
162 std::to_string(invMassType.getRank()));
163 return nullptr;
164}
165
166Value impulse::computeKineticEnergy(OpBuilder &builder, Location loc,
167 Value momentum, Value invMass,
168 RankedTensorType positionType) {
169 auto elemType = positionType.getElementType();
170 auto scalarType = RankedTensorType::get({}, elemType);
171
172 auto halfConst = arith::ConstantOp::create(
173 builder, loc, scalarType,
174 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.5)));
175
176 // v = M^-1 @ p
177 auto v =
178 applyInverseMassMatrix(builder, loc, invMass, momentum, positionType);
179
180 // K = 0.5 * p^T @ v
181 // For 2D tensors [1, N], contract over both dimensions to get scalar
182 auto pDotV = impulse::DotOp::create(
183 builder, loc, scalarType, momentum, v, builder.getDenseI64ArrayAttr({}),
184 builder.getDenseI64ArrayAttr({}), builder.getDenseI64ArrayAttr({0, 1}),
185 builder.getDenseI64ArrayAttr({0, 1}));
186
187 return arith::MulFOp::create(builder, loc, halfConst, pDotV);
188}
189
190Value impulse::computeMassMatrixSqrt(OpBuilder &builder, Location loc,
191 Value invMass,
192 RankedTensorType positionType) {
193 if (!invMass) {
194 return Value();
195 }
196
197 auto invMassType = cast<RankedTensorType>(invMass.getType());
198 auto elemType = invMassType.getElementType();
199
200 if (invMassType.getRank() == 1) {
201 // Diagonal: mass_matrix_sqrt = 1/sqrt(invMass)
202 auto sqrtInvMass = math::SqrtOp::create(builder, loc, invMass);
203 auto onesVector = arith::ConstantOp::create(
204 builder, loc, invMassType,
205 DenseElementsAttr::get(invMassType,
206 builder.getFloatAttr(elemType, 1.0)));
207 return arith::DivFOp::create(builder, loc, onesVector, sqrtInvMass);
208 } else {
209 // Dense: mass_matrix_sqrt = M^{1/2}
210 // TODO: improve
211 // Reference:
212 // https://github.com/pyro-ppl/numpyro/blob/6a9cb9a530fe53897edb6c472368e58965b034e4/numpyro/infer/hmc_util.py#L499
213 auto reversedInvMass = reverseRowsAndColumns(builder, loc, invMass);
214 auto L_reversed =
215 impulse::CholeskyOp::create(builder, loc, invMassType, reversedInvMass,
216 /*lower=*/builder.getBoolAttr(true));
217 auto massMatrixSqrtInvT = reverseRowsAndColumns(builder, loc, L_reversed);
218 auto identityMatrix = createIdentityMatrix(builder, loc, invMassType);
219 auto massMatrixSqrt = impulse::TriangularSolveOp::create(
220 builder, loc, invMassType, massMatrixSqrtInvT, identityMatrix,
221 /*left_side=*/builder.getBoolAttr(true),
222 /*lower=*/builder.getBoolAttr(false),
223 /*unit_diagonal=*/builder.getBoolAttr(false),
224 /*transpose_a=*/
225 impulse::TransposeAttr::get(builder.getContext(),
226 impulse::Transpose::TRANSPOSE));
227
228 return massMatrixSqrt;
229 }
230}
231
232std::pair<Value, Value>
233impulse::sampleMomentum(OpBuilder &builder, Location loc, Value rng,
234 Value invMass, Value massMatrixSqrt,
235 RankedTensorType positionType, bool debugDump) {
236 auto elemType = positionType.getElementType();
237 auto scalarType = RankedTensorType::get({}, elemType);
238
239 auto zeroConst = arith::ConstantOp::create(
240 builder, loc, scalarType,
241 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.0)));
242 auto oneConst = arith::ConstantOp::create(
243 builder, loc, scalarType,
244 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 1.0)));
245
246 rng = conditionalDump(builder, loc, rng, "sampleMomentum: input rng state",
247 debugDump);
248
249 // Sample eps ~ N(0, I)
250 auto randomOp = impulse::RandomOp::create(
251 builder, loc, TypeRange{rng.getType(), positionType}, rng, zeroConst,
252 oneConst,
253 impulse::RngDistributionAttr::get(builder.getContext(),
254 impulse::RngDistribution::NORMAL));
255
256 auto rngOut = randomOp.getOutputRngState();
257 auto eps = randomOp.getResult();
258
259 if (!massMatrixSqrt) {
260 return {eps, rngOut};
261 }
262
263 auto massMatrixSqrtType = cast<RankedTensorType>(massMatrixSqrt.getType());
264
265 if (massMatrixSqrtType.getRank() == 1) {
266 // Diagonal: p = massMatrixSqrt * eps (element-wise)
267 auto diagSqrt =
268 impulse::ReshapeOp::create(builder, loc, positionType, massMatrixSqrt);
269 auto p = arith::MulFOp::create(builder, loc, diagSqrt, eps);
270 return {p, rngOut};
271 } else {
272 // Dense: p = massMatrixSqrt @ eps
273 auto p = impulse::DotOp::create(
274 builder, loc, positionType, eps, massMatrixSqrt,
275 builder.getDenseI64ArrayAttr({}), builder.getDenseI64ArrayAttr({}),
276 builder.getDenseI64ArrayAttr({1}), builder.getDenseI64ArrayAttr({1}));
277 return {p, rngOut};
278 }
279}
280
281static Value scatterPositionToTrace(OpBuilder &builder, Location loc,
282 Value position2d, Value fullTrace,
283 const HMCContext &ctx) {
284 auto elemType =
285 cast<RankedTensorType>(ctx.stepSize.getType()).getElementType();
286 auto traceType = RankedTensorType::get({1, ctx.getFullTraceSize()}, elemType);
287 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
288 auto c0 = arith::ConstantOp::create(
289 builder, loc, i64TensorType,
290 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
291
292 Value result = fullTrace;
293 for (const auto &info : ctx.supports) {
294 auto sliceType = RankedTensorType::get({1, info.size}, elemType);
295 auto posOffset = arith::ConstantOp::create(
296 builder, loc, i64TensorType,
297 DenseElementsAttr::get(i64TensorType,
298 builder.getI64IntegerAttr(info.offset)));
299 SmallVector<Value> extractIndices{c0, posOffset};
300 auto slice = impulse::DynamicSliceOp::create(
301 builder, loc, sliceType, position2d, extractIndices,
302 builder.getDenseI64ArrayAttr({1, info.size}));
303
304 auto traceOffset = arith::ConstantOp::create(
305 builder, loc, i64TensorType,
306 DenseElementsAttr::get(i64TensorType,
307 builder.getI64IntegerAttr(info.traceOffset)));
308 SmallVector<Value> updateIndices{c0, traceOffset};
309 result = impulse::DynamicUpdateSliceOp::create(
310 builder, loc, traceType, result, slice, updateIndices);
311 }
312 return result;
313}
314
315static Value gatherPositionFromTrace(OpBuilder &builder, Location loc,
316 Value fullTrace, const HMCContext &ctx) {
317 auto elemType =
318 cast<RankedTensorType>(ctx.stepSize.getType()).getElementType();
319 auto positionType2d = RankedTensorType::get({1, ctx.positionSize}, elemType);
320 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
321 auto c0 = arith::ConstantOp::create(
322 builder, loc, i64TensorType,
323 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
324
325 auto zeroConst = arith::ConstantOp::create(
326 builder, loc, positionType2d,
327 DenseElementsAttr::get(positionType2d,
328 builder.getFloatAttr(elemType, 0.0)));
329 Value result = zeroConst;
330 for (const auto &info : ctx.supports) {
331 auto sliceType = RankedTensorType::get({1, info.size}, elemType);
332 auto traceOffset = arith::ConstantOp::create(
333 builder, loc, i64TensorType,
334 DenseElementsAttr::get(i64TensorType,
335 builder.getI64IntegerAttr(info.traceOffset)));
336 SmallVector<Value> extractIndices{c0, traceOffset};
337 auto slice = impulse::DynamicSliceOp::create(
338 builder, loc, sliceType, fullTrace, extractIndices,
339 builder.getDenseI64ArrayAttr({1, info.size}));
340
341 auto posOffset = arith::ConstantOp::create(
342 builder, loc, i64TensorType,
343 DenseElementsAttr::get(i64TensorType,
344 builder.getI64IntegerAttr(info.offset)));
345 SmallVector<Value> updateIndices{c0, posOffset};
346 result = impulse::DynamicUpdateSliceOp::create(
347 builder, loc, positionType2d, result, slice, updateIndices);
348 }
349 return result;
350}
351
353 Location loc,
354 Value position, Value rng,
355 const HMCContext &ctx) {
356 auto positionType = ctx.getPositionType();
357 auto scalarType = ctx.getScalarType();
358 auto elemType = ctx.getElementType();
359
360 auto gradSeed = arith::ConstantOp::create(
361 builder, loc, scalarType,
362 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 1.0)));
363
364 bool isCustomLogpdf = ctx.hasCustomLogpdf();
365 auto flatType = RankedTensorType::get({ctx.positionSize}, elemType);
366 Value autodiffPosition = position;
367 auto autodiffPositionType = positionType;
368 auto autodiffGradType = positionType;
369 if (isCustomLogpdf) {
370 autodiffPosition =
371 impulse::ReshapeOp::create(builder, loc, flatType, position);
372 autodiffPositionType = flatType;
373 autodiffGradType = flatType;
374 }
375
376 SmallVector<Value> autodiffInputs{autodiffPosition, gradSeed};
377 SmallVector<NamedAttribute> adAttrs{
378 builder.getNamedAttr(
379 "activity",
380 builder.getArrayAttr({enzyme::ActivityAttr::get(
381 builder.getContext(), enzyme::Activity::enzyme_active)})),
382 builder.getNamedAttr(
383 "ret_activity",
384 builder.getArrayAttr(
385 {enzyme::ActivityAttr::get(builder.getContext(),
386 enzyme::Activity::enzyme_active),
387 enzyme::ActivityAttr::get(builder.getContext(),
388 enzyme::Activity::enzyme_const)})),
389 };
390 if (ctx.autodiffAttrs) {
391 for (auto attr : ctx.autodiffAttrs)
392 adAttrs.push_back(attr);
393 }
394 auto autodiffOp = enzyme::AutoDiffRegionOp::create(
395 builder, loc, TypeRange{scalarType, rng.getType(), autodiffGradType},
396 autodiffInputs, adAttrs);
397
398 Block *autodiffBlock = builder.createBlock(&autodiffOp.getBody());
399 autodiffBlock->addArgument(autodiffPositionType, loc);
400
401 builder.setInsertionPointToStart(autodiffBlock);
402 Value qArg = autodiffBlock->getArgument(0);
403
404 if (isCustomLogpdf) {
405 SmallVector<Value> callArgs;
406 callArgs.push_back(qArg);
407 callArgs.append(ctx.fnInputs.begin(), ctx.fnInputs.end());
408 auto callOp = func::CallOp::create(builder, loc, ctx.logpdfFn,
409 TypeRange{scalarType}, callArgs);
410 Value U = arith::NegFOp::create(builder, loc, callOp.getResult(0));
411 // TODO(#2695): handle hybrid case
412 enzyme::YieldOp::create(builder, loc, {U, rng});
413 } else {
414 auto traceType =
415 RankedTensorType::get({1, ctx.getFullTraceSize()}, elemType);
416
417 Value q_constrained = constrainPosition(builder, loc, qArg, ctx.supports);
418 Value fullTrace = scatterPositionToTrace(builder, loc, q_constrained,
419 ctx.originalTrace, ctx);
420
421 SmallVector<Value> generateInputs;
422 generateInputs.push_back(rng);
423 generateInputs.append(ctx.fnInputs.begin(), ctx.fnInputs.end());
424
425 SmallVector<Type> generateResultTypes;
426 generateResultTypes.push_back(traceType);
427 generateResultTypes.push_back(scalarType);
428 generateResultTypes.append(ctx.fnResultTypes.begin(),
429 ctx.fnResultTypes.end());
430
431 auto generateOp = impulse::GenerateOp::create(
432 builder, loc, generateResultTypes, ctx.fn, generateInputs, fullTrace,
433 ctx.allAddresses, ctx.allAddresses, builder.getStringAttr(""));
434
435 Value negWeight =
436 arith::NegFOp::create(builder, loc, generateOp.getWeight());
437 Value jacobianCorrection =
438 computeTotalJacobianCorrection(builder, loc, qArg, ctx.supports);
439 Value U =
440 arith::SubFOp::create(builder, loc, negWeight, jacobianCorrection);
441
442 SmallVector<Value> yieldValues{U, generateOp.getResult(2)};
443 enzyme::YieldOp::create(builder, loc, yieldValues);
444 }
445
446 builder.setInsertionPointAfter(autodiffOp);
447
448 Value grad = autodiffOp.getResult(2);
449 if (isCustomLogpdf) {
450 grad = impulse::ReshapeOp::create(builder, loc, positionType, grad);
451 }
452
453 return {
454 autodiffOp.getResult(0), // U
455 grad, // grad
456 autodiffOp.getResult(1) // rng
457 };
458}
459
461 Location loc,
462 const IntegratorState &leaf,
463 Value rng, Value direction,
464 const HMCContext &ctx) {
465 auto positionType = ctx.getPositionType();
466 auto scalarType = ctx.getScalarType();
467 auto elemType = ctx.getElementType();
468
469 auto negStepSize = arith::NegFOp::create(builder, loc, ctx.stepSize);
470 Value signedStepSize = impulse::SelectOp::create(
471 builder, loc, scalarType, direction, ctx.stepSize, negStepSize);
472
473 auto halfConst = arith::ConstantOp::create(
474 builder, loc, scalarType,
475 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.5)));
476
477 ArrayRef<int64_t> shape = positionType.getShape();
478 auto stepSizeBroadcast =
479 BroadcastOp::create(builder, loc, positionType, signedStepSize,
480 builder.getDenseI64ArrayAttr(shape));
481 auto halfStep =
482 arith::MulFOp::create(builder, loc, halfConst, signedStepSize);
483 auto halfStepBroadcast =
484 BroadcastOp::create(builder, loc, positionType, halfStep,
485 builder.getDenseI64ArrayAttr(shape));
486
487 // 1. Half step momentum: p_half = p - 0.5 * eps * grad
488 auto deltaP1 =
489 arith::MulFOp::create(builder, loc, halfStepBroadcast, leaf.grad);
490 Value pHalf = arith::SubFOp::create(builder, loc, leaf.p, deltaP1);
491
492 // 2. Full step position: q_new = q + eps * M^-1 * p_half
493 Value v =
494 applyInverseMassMatrix(builder, loc, ctx.invMass, pHalf, positionType);
495 auto deltaQ = arith::MulFOp::create(builder, loc, stepSizeBroadcast, v);
496 Value qNew = arith::AddFOp::create(builder, loc, leaf.q, deltaQ);
497
498 // 3. Compute gradient at new position
499 auto gradResult = computePotentialAndGradient(builder, loc, qNew, rng, ctx);
500
501 // 4. Final half step momentum: p_new = p_half - 0.5 * eps * grad_new
502 auto deltaP2 =
503 arith::MulFOp::create(builder, loc, halfStepBroadcast, gradResult.grad);
504 Value pNew = arith::SubFOp::create(builder, loc, pHalf, deltaP2);
505
506 return {qNew, pNew, gradResult.grad, gradResult.U, gradResult.rng};
507}
508
509Value impulse::checkTurning(OpBuilder &builder, Location loc, Value pLeft,
510 Value pRight, Value pSum, const NUTSContext &ctx) {
511 auto positionType = ctx.getPositionType();
512 auto scalarType = ctx.getScalarType();
513 auto elemType = ctx.getElementType();
514
515 auto zeroConst = arith::ConstantOp::create(
516 builder, loc, scalarType,
517 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.0)));
518 auto halfConst = arith::ConstantOp::create(
519 builder, loc, scalarType,
520 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.5)));
521
522 Value vLeft =
523 applyInverseMassMatrix(builder, loc, ctx.invMass, pLeft, positionType);
524 Value vRight =
525 applyInverseMassMatrix(builder, loc, ctx.invMass, pRight, positionType);
526
527 // p_sum_centered = p_sum - (p_left + p_right) / 2
528 auto halfBroadcast = BroadcastOp::create(
529 builder, loc, positionType, halfConst,
530 builder.getDenseI64ArrayAttr(positionType.getShape()));
531
532 auto pLeftPlusPRight = arith::AddFOp::create(builder, loc, pLeft, pRight);
533 auto halfSum =
534 arith::MulFOp::create(builder, loc, halfBroadcast, pLeftPlusPRight);
535 Value pSumCentered = arith::SubFOp::create(builder, loc, pSum, halfSum);
536
537 auto leftAngle = impulse::DotOp::create(
538 builder, loc, scalarType, vLeft, pSumCentered,
539 builder.getDenseI64ArrayAttr({}), builder.getDenseI64ArrayAttr({}),
540 builder.getDenseI64ArrayAttr({0, 1}),
541 builder.getDenseI64ArrayAttr({0, 1}));
542 auto rightAngle = impulse::DotOp::create(
543 builder, loc, scalarType, vRight, pSumCentered,
544 builder.getDenseI64ArrayAttr({}), builder.getDenseI64ArrayAttr({}),
545 builder.getDenseI64ArrayAttr({0, 1}),
546 builder.getDenseI64ArrayAttr({0, 1}));
547
548 // turning = (left_angle <= 0) OR (right_angle <= 0)
549 auto leftNeg = arith::CmpFOp::create(builder, loc, arith::CmpFPredicate::OLE,
550 leftAngle, zeroConst);
551 auto rightNeg = arith::CmpFOp::create(builder, loc, arith::CmpFPredicate::OLE,
552 rightAngle, zeroConst);
553
554 return arith::OrIOp::create(builder, loc, leftNeg, rightNeg);
555}
556
557Value impulse::computeUniformTransitionProb(OpBuilder &builder, Location loc,
558 Value currentWeight,
559 Value newWeight) {
560 Value weightDiff =
561 arith::SubFOp::create(builder, loc, newWeight, currentWeight);
562 return impulse::LogisticOp::create(builder, loc, weightDiff.getType(),
563 weightDiff);
564}
565
566Value impulse::computeBiasedTransitionProb(OpBuilder &builder, Location loc,
567 Value currentWeight, Value newWeight,
568 Value turning, Value diverging) {
569 auto resultType = cast<RankedTensorType>(currentWeight.getType());
570 auto elemType = resultType.getElementType();
571
572 auto zeroConst = arith::ConstantOp::create(
573 builder, loc, resultType,
574 DenseElementsAttr::get(resultType, builder.getFloatAttr(elemType, 0.0)));
575 auto oneConst = arith::ConstantOp::create(
576 builder, loc, resultType,
577 DenseElementsAttr::get(resultType, builder.getFloatAttr(elemType, 1.0)));
578
579 Value weightDiff =
580 arith::SubFOp::create(builder, loc, newWeight, currentWeight);
581 Value expDiff = math::ExpOp::create(builder, loc, weightDiff);
582 Value clippedProb =
583 arith::MinimumFOp::create(builder, loc, oneConst, expDiff);
584 Value turningOrDiverging =
585 arith::OrIOp::create(builder, loc, turning, diverging);
586 return arith::SelectOp::create(builder, loc, resultType, turningOrDiverging,
587 zeroConst, clippedProb);
588}
589
590NUTSTreeState impulse::combineTrees(OpBuilder &builder, Location loc,
591 const NUTSTreeState &tree,
592 const NUTSTreeState &subTree,
593 Value direction, Value rng, bool biased,
594 const NUTSContext &ctx) {
595 auto positionType = ctx.getPositionType();
596 auto scalarType = ctx.getScalarType();
597 auto elemType = ctx.getElementType();
598 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
599
600 auto zeroConst = arith::ConstantOp::create(
601 builder, loc, scalarType,
602 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.0)));
603 auto oneConst = arith::ConstantOp::create(
604 builder, loc, scalarType,
605 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 1.0)));
606
607 auto qLeft = impulse::SelectOp::create(builder, loc, positionType, direction,
608 tree.q_left, subTree.q_left);
609 auto pLeft = impulse::SelectOp::create(builder, loc, positionType, direction,
610 tree.p_left, subTree.p_left);
611 auto gradLeft = impulse::SelectOp::create(
612 builder, loc, positionType, direction, tree.grad_left, subTree.grad_left);
613 auto qRight = impulse::SelectOp::create(builder, loc, positionType, direction,
614 subTree.q_right, tree.q_right);
615 auto pRight = impulse::SelectOp::create(builder, loc, positionType, direction,
616 subTree.p_right, tree.p_right);
617 auto gradRight =
618 impulse::SelectOp::create(builder, loc, positionType, direction,
619 subTree.grad_right, tree.grad_right);
620
621 auto combinedWeight = impulse::LogAddExpOp::create(
622 builder, loc, scalarType, tree.weight, subTree.weight);
623
624 // Compute transition probability
625 Value transitionProb;
626 if (biased) {
627 transitionProb =
628 computeBiasedTransitionProb(builder, loc, tree.weight, subTree.weight,
629 subTree.turning, subTree.diverging);
630 } else {
631 transitionProb =
632 computeUniformTransitionProb(builder, loc, tree.weight, subTree.weight);
633 }
634
635 auto randomOp = impulse::RandomOp::create(
636 builder, loc, TypeRange{rng.getType(), scalarType}, rng, zeroConst,
637 oneConst,
638 impulse::RngDistributionAttr::get(builder.getContext(),
639 impulse::RngDistribution::UNIFORM));
640 auto rngOut = randomOp.getOutputRngState();
641 auto uniformSample = randomOp.getResult();
642
643 auto acceptNew = arith::CmpFOp::create(
644 builder, loc, arith::CmpFPredicate::OLT, uniformSample, transitionProb);
645
646 auto qProposal =
647 impulse::SelectOp::create(builder, loc, positionType, acceptNew,
648 subTree.q_proposal, tree.q_proposal);
649 auto gradProposal =
650 impulse::SelectOp::create(builder, loc, positionType, acceptNew,
651 subTree.grad_proposal, tree.grad_proposal);
652 auto UProposal = impulse::SelectOp::create(
653 builder, loc, scalarType, acceptNew, subTree.U_proposal, tree.U_proposal);
654 auto HProposal = impulse::SelectOp::create(
655 builder, loc, scalarType, acceptNew, subTree.H_proposal, tree.H_proposal);
656
657 auto oneI64 = arith::ConstantOp::create(
658 builder, loc, i64TensorType,
659 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
660 auto combinedDepth = arith::AddIOp::create(builder, loc, tree.depth, oneI64);
661
662 Value combinedTurning;
663 if (biased) {
664 auto turningCheck = checkTurning(
665 builder, loc, pLeft, pRight,
666 arith::AddFOp::create(builder, loc, tree.p_sum, subTree.p_sum), ctx);
667 combinedTurning =
668 arith::OrIOp::create(builder, loc, subTree.turning, turningCheck);
669 } else {
670 combinedTurning = tree.turning;
671 }
672
673 auto combinedDiverging =
674 arith::OrIOp::create(builder, loc, tree.diverging, subTree.diverging);
675 auto sumAcceptProbs = arith::AddFOp::create(
676 builder, loc, tree.sum_accept_probs, subTree.sum_accept_probs);
677 auto numProposals = arith::AddIOp::create(builder, loc, tree.num_proposals,
678 subTree.num_proposals);
679 auto pSum = arith::AddFOp::create(builder, loc, tree.p_sum, subTree.p_sum);
680
681 return {.q_left = qLeft,
682 .p_left = pLeft,
683 .grad_left = gradLeft,
684 .q_right = qRight,
685 .p_right = pRight,
686 .grad_right = gradRight,
687 .q_proposal = qProposal,
688 .grad_proposal = gradProposal,
689 .U_proposal = UProposal,
690 .H_proposal = HProposal,
691 .depth = combinedDepth,
692 .weight = combinedWeight,
693 .turning = combinedTurning,
694 .diverging = combinedDiverging,
695 .sum_accept_probs = sumAcceptProbs,
696 .num_proposals = numProposals,
697 .p_sum = pSum,
698 .rng = rngOut};
699}
700
701InitialHMCState impulse::InitHMC(OpBuilder &builder, Location loc, Value rng,
702 const HMCContext &ctx, Value initialPosition,
703 bool debugDump) {
704 auto positionType = ctx.getPositionType();
705 auto scalarType = ctx.getScalarType();
706 auto elemType = ctx.getElementType();
707
708 auto initSplit = impulse::RandomSplitOp::create(
709 builder, loc, TypeRange{rng.getType(), rng.getType()}, rng);
710 auto kernelSplit = impulse::RandomSplitOp::create(
711 builder, loc, TypeRange{rng.getType(), rng.getType(), rng.getType()},
712 initSplit.getResult(0));
713 auto rngForSampleKernel = kernelSplit.getResult(0);
714 auto rngForAutodiff = kernelSplit.getResult(1);
715
716 Value q0;
717 Value U0;
718
719 if (ctx.hasCustomLogpdf()) {
720 q0 = initialPosition;
721 auto flatType = RankedTensorType::get({ctx.positionSize}, elemType);
722 auto q0Flat = impulse::ReshapeOp::create(builder, loc, flatType, q0);
723 SmallVector<Value> callArgs;
724 callArgs.push_back(q0Flat);
725 callArgs.append(ctx.fnInputs.begin(), ctx.fnInputs.end());
726 auto callOp = func::CallOp::create(builder, loc, ctx.logpdfFn,
727 TypeRange{scalarType}, callArgs);
728 U0 = arith::NegFOp::create(builder, loc, callOp.getResult(0));
729 } else {
730 auto fullTraceType =
731 RankedTensorType::get({1, ctx.getFullTraceSize()}, elemType);
732
733 // 1. Extract initial position vector (constrained)
734 auto q0_constrained =
735 gatherPositionFromTrace(builder, loc, ctx.originalTrace, ctx);
736
737 // 2. Unconstrain to get position vector for HMC
738 q0 = unconstrainPosition(builder, loc, q0_constrained, ctx.supports);
739
740 // 3. Compute initial potential energy: U0 = -weight + correction
741 Value fullTraceInit = scatterPositionToTrace(builder, loc, q0_constrained,
742 ctx.originalTrace, ctx);
743
744 SmallVector<Value> generateInputsInit;
745 generateInputsInit.push_back(rngForAutodiff);
746 generateInputsInit.append(ctx.fnInputs.begin(), ctx.fnInputs.end());
747
748 SmallVector<Type> generateResultTypesInit;
749 generateResultTypesInit.push_back(fullTraceType);
750 generateResultTypesInit.push_back(scalarType);
751 generateResultTypesInit.append(ctx.fnResultTypes.begin(),
752 ctx.fnResultTypes.end());
753
754 auto generateOpInit = impulse::GenerateOp::create(
755 builder, loc, generateResultTypesInit, ctx.fn, generateInputsInit,
756 fullTraceInit, ctx.allAddresses, ctx.allAddresses,
757 builder.getStringAttr(""));
758
759 auto weight0 = generateOpInit.getWeight();
760 auto negWeight0 = arith::NegFOp::create(builder, loc, weight0);
761 auto jacobian0 =
762 computeTotalJacobianCorrection(builder, loc, q0, ctx.supports);
763 U0 = arith::SubFOp::create(builder, loc, negWeight0, jacobian0);
764 }
765
766 // 4. Compute initial gradient at q0
767 bool isCustomLogpdf = ctx.hasCustomLogpdf();
768 auto flatType = RankedTensorType::get({ctx.positionSize}, elemType);
769 Value autodiffQ0 = q0;
770 auto autodiffQ0Type = positionType;
771 auto autodiffGradType = positionType;
772 if (isCustomLogpdf) {
773 autodiffQ0 = impulse::ReshapeOp::create(builder, loc, flatType, q0);
774 autodiffQ0Type = flatType;
775 autodiffGradType = flatType;
776 }
777
778 auto gradSeedInit = arith::ConstantOp::create(
779 builder, loc, scalarType,
780 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 1.0)));
781 SmallVector<Value> autodiffInputs{autodiffQ0, gradSeedInit};
782 SmallVector<NamedAttribute> adInitAttrs{
783 builder.getNamedAttr(
784 "activity",
785 builder.getArrayAttr({enzyme::ActivityAttr::get(
786 builder.getContext(), enzyme::Activity::enzyme_active)})),
787 builder.getNamedAttr(
788 "ret_activity",
789 builder.getArrayAttr(
790 {enzyme::ActivityAttr::get(builder.getContext(),
791 enzyme::Activity::enzyme_active),
792 enzyme::ActivityAttr::get(builder.getContext(),
793 enzyme::Activity::enzyme_const)})),
794 };
795 if (ctx.autodiffAttrs) {
796 for (auto attr : ctx.autodiffAttrs)
797 adInitAttrs.push_back(attr);
798 }
799 auto autodiffInit = enzyme::AutoDiffRegionOp::create(
800 builder, loc,
801 TypeRange{scalarType, rngForAutodiff.getType(), autodiffGradType},
802 autodiffInputs, adInitAttrs);
803
804 Block *autodiffInitBlock = builder.createBlock(&autodiffInit.getBody());
805 autodiffInitBlock->addArgument(autodiffQ0Type, loc);
806
807 builder.setInsertionPointToStart(autodiffInitBlock);
808 auto q0Arg = autodiffInitBlock->getArgument(0);
809
810 if (isCustomLogpdf) {
811 SmallVector<Value> callArgs;
812 callArgs.push_back(q0Arg);
813 callArgs.append(ctx.fnInputs.begin(), ctx.fnInputs.end());
814 auto callOpInner = func::CallOp::create(builder, loc, ctx.logpdfFn,
815 TypeRange{scalarType}, callArgs);
816 auto U0_init =
817 arith::NegFOp::create(builder, loc, callOpInner.getResult(0));
818 enzyme::YieldOp::create(builder, loc, {U0_init, rngForAutodiff});
819 } else {
820 auto fullTraceType =
821 RankedTensorType::get({1, ctx.getFullTraceSize()}, elemType);
822 auto q0Arg_constrained =
823 constrainPosition(builder, loc, q0Arg, ctx.supports);
824 Value fullTraceInner = scatterPositionToTrace(
825 builder, loc, q0Arg_constrained, ctx.originalTrace, ctx);
826
827 SmallVector<Value> generateInputsInner;
828 generateInputsInner.push_back(rngForAutodiff);
829 generateInputsInner.append(ctx.fnInputs.begin(), ctx.fnInputs.end());
830
831 SmallVector<Type> generateResultTypesInner;
832 generateResultTypesInner.push_back(fullTraceType);
833 generateResultTypesInner.push_back(scalarType);
834 generateResultTypesInner.append(ctx.fnResultTypes.begin(),
835 ctx.fnResultTypes.end());
836
837 auto generateOpInner = impulse::GenerateOp::create(
838 builder, loc, generateResultTypesInner, ctx.fn, generateInputsInner,
839 fullTraceInner, ctx.allAddresses, ctx.allAddresses,
840 builder.getStringAttr(""));
841
842 auto negWeightInit =
843 arith::NegFOp::create(builder, loc, generateOpInner.getWeight());
844 auto jacobianInit =
845 computeTotalJacobianCorrection(builder, loc, q0Arg, ctx.supports);
846 auto U0_init =
847 arith::SubFOp::create(builder, loc, negWeightInit, jacobianInit);
848
849 SmallVector<Value> yieldValues{U0_init, generateOpInner.getResult(2)};
850 enzyme::YieldOp::create(builder, loc, yieldValues);
851 }
852 builder.setInsertionPointAfter(autodiffInit);
853
854 Value grad0 = autodiffInit.getResult(2);
855 if (isCustomLogpdf) {
856 grad0 = impulse::ReshapeOp::create(builder, loc, positionType, grad0);
857 }
858
859 return {q0, U0, grad0, rngForSampleKernel};
860}
861
862MCMCKernelResult impulse::SampleHMC(OpBuilder &builder, Location loc, Value q,
863 Value grad, Value U, Value rng,
864 const HMCContext &ctx, bool debugDump) {
865 auto positionType = ctx.getPositionType();
866 auto scalarType = ctx.getScalarType();
867 auto elemType = ctx.getElementType();
868 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
869 auto i1TensorType = RankedTensorType::get({}, builder.getI1Type());
870
871 // 0. Compute num_steps and adjusted step_size
872 // num_steps = ceil(trajectory_length / step_size)
873 // adjusted_step_size = trajectory_length / num_steps
874 auto trajDivStep =
875 arith::DivFOp::create(builder, loc, ctx.trajectoryLength, ctx.stepSize);
876 auto numStepsF64 = math::CeilOp::create(builder, loc, trajDivStep);
877 auto numSteps =
878 arith::FPToSIOp::create(builder, loc, i64TensorType, numStepsF64);
879 auto adjustedStepSize =
880 arith::DivFOp::create(builder, loc, ctx.trajectoryLength, numStepsF64);
881
882 auto adjustedCtx = ctx.withStepSize(adjustedStepSize);
883
884 // 1. Split RNG: [rngNext, rngMomentum, rngTransition]
885 auto sampleKernelSplit = impulse::RandomSplitOp::create(
886 builder, loc, TypeRange{rng.getType(), rng.getType(), rng.getType()},
887 rng);
888 auto rngNext = sampleKernelSplit.getResult(0);
889 auto rngMomentum = sampleKernelSplit.getResult(1);
890 auto rngTransition = sampleKernelSplit.getResult(2);
891
892 // 2. Sample fresh momentum p ~ N(0, M)
893 Value rngForMomentum = rngMomentum;
894 if (!ctx.hasCustomLogpdf()) {
895 auto momSplit = impulse::RandomSplitOp::create(
896 builder, loc, TypeRange{rng.getType(), rng.getType()}, rngMomentum);
897 rngForMomentum = momSplit.getResult(0);
898 }
899 auto [p0, rngAfterMomentum] =
900 sampleMomentum(builder, loc, rngForMomentum, ctx.invMass,
901 ctx.massMatrixSqrt, positionType, debugDump);
902
903 // 3. Compute K0 = 0.5 * p^T * M^-1 * p
904 auto K0 = computeKineticEnergy(builder, loc, p0, ctx.invMass, positionType);
905
906 // 4. Compute H0 = U + K0
907 auto H0 = arith::AddFOp::create(builder, loc, U, K0);
908
909 // 5. Leapfrog integration loop
910 auto direction = arith::ConstantOp::create(
911 builder, loc, i1TensorType,
912 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(true)));
913
914 auto c0 = arith::ConstantOp::create(
915 builder, loc, i64TensorType,
916 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
917 auto c1 = arith::ConstantOp::create(
918 builder, loc, i64TensorType,
919 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
920
921 // Loop carries: [q, p, grad, U, rng]
922 SmallVector<Type> loopResultTypes = {positionType, positionType, positionType,
923 scalarType, rngTransition.getType()};
924 auto forLoopOp =
925 impulse::ForOp::create(builder, loc, loopResultTypes, c0, numSteps, c1,
926 ValueRange{q, p0, grad, U, rngTransition});
927
928 Block *loopBody = builder.createBlock(&forLoopOp.getRegion());
929 loopBody->addArgument(i64TensorType, loc); // iv
930 loopBody->addArgument(positionType, loc); // q
931 loopBody->addArgument(positionType, loc); // p
932 loopBody->addArgument(positionType, loc); // gradient
933 loopBody->addArgument(scalarType, loc); // U
934 loopBody->addArgument(rngTransition.getType(), loc); // rng
935
936 builder.setInsertionPointToStart(loopBody);
937 auto qLoop = loopBody->getArgument(1);
938 auto pLoop = loopBody->getArgument(2);
939 auto gradLoop = loopBody->getArgument(3);
940 auto rngLoop = loopBody->getArgument(5);
941
942 IntegratorState leaf = {qLoop, pLoop, gradLoop};
943 auto step = computeIntegrationStep(builder, loc, leaf, rngLoop, direction,
944 adjustedCtx);
945
946 // Yield [q, p, grad, U, rng]
947 impulse::YieldOp::create(
948 builder, loc, ValueRange{step.q, step.p, step.grad, step.U, step.rng});
949
950 builder.setInsertionPointAfter(forLoopOp);
951 auto qProposal = forLoopOp.getResult(0);
952 auto pProposal = forLoopOp.getResult(1);
953 auto gradProposal = forLoopOp.getResult(2);
954 auto UProposal = forLoopOp.getResult(3);
955 auto rngAfterLeapfrog = forLoopOp.getResult(4);
956
957 // 6. Compute K1, H1 for proposal
958 auto K1 =
959 computeKineticEnergy(builder, loc, pProposal, ctx.invMass, positionType);
960 auto H1 = arith::AddFOp::create(builder, loc, UProposal, K1);
961
962 // 7. MH accept/reject
963 auto zeroConst = arith::ConstantOp::create(
964 builder, loc, scalarType,
965 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.0)));
966 auto oneConst = arith::ConstantOp::create(
967 builder, loc, scalarType,
968 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 1.0)));
969
970 // α = min(1, exp(H0 - H1))
971 auto dH = arith::SubFOp::create(builder, loc, H0, H1);
972 auto expDH = math::ExpOp::create(builder, loc, dH);
973 auto accProb = arith::MinimumFOp::create(builder, loc, oneConst, expDH);
974
975 // u ~ Uniform(0, 1)
976 auto randomOp = impulse::RandomOp::create(
977 builder, loc, TypeRange{rngAfterLeapfrog.getType(), scalarType},
978 rngAfterLeapfrog, zeroConst, oneConst,
979 impulse::RngDistributionAttr::get(builder.getContext(),
980 impulse::RngDistribution::UNIFORM));
981 auto randUniform = randomOp.getResult();
982
983 // accepted = u < α
984 auto acceptedTensor = arith::CmpFOp::create(
985 builder, loc, arith::CmpFPredicate::OLT, randUniform, accProb);
986
987 // HMC doesn't collect divergence information so set to false
988 auto falseConst = arith::ConstantOp::create(
989 builder, loc, i1TensorType,
990 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(false)));
991
992 // 8. Select between original and proposal
993 auto qFinal = impulse::SelectOp::create(builder, loc, positionType,
994 acceptedTensor, qProposal, q);
995 auto gradFinal = impulse::SelectOp::create(
996 builder, loc, positionType, acceptedTensor, gradProposal, grad);
997 auto UFinal = impulse::SelectOp::create(builder, loc, scalarType,
998 acceptedTensor, UProposal, U);
999
1000 return {qFinal, gradFinal, UFinal, acceptedTensor,
1001 falseConst, accProb, rngNext};
1002}
1003
1004MCMCKernelResult impulse::SampleNUTS(OpBuilder &builder, Location loc, Value q,
1005 Value grad, Value U, Value rng,
1006 const NUTSContext &ctx, bool debugDump) {
1007 auto positionType = ctx.getPositionType();
1008 auto scalarType = ctx.getScalarType();
1009 auto elemType = ctx.getElementType();
1010 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1011 auto i1TensorType = RankedTensorType::get({}, builder.getI1Type());
1012
1013 // 1. Split RNG: [rngNext, rngMomentum, rngTree]
1014 auto sampleKernelSplit = impulse::RandomSplitOp::create(
1015 builder, loc, TypeRange{rng.getType(), rng.getType(), rng.getType()},
1016 rng);
1017 auto rngNext = sampleKernelSplit.getResult(0);
1018 auto rngMomentum = sampleKernelSplit.getResult(1);
1019 auto rngTree = sampleKernelSplit.getResult(2);
1020
1021 // 2. Sample fresh momentum p ~ N(0, M)
1022 Value rngForMomentum = rngMomentum;
1023 if (!ctx.hasCustomLogpdf()) {
1024 auto momSplit = impulse::RandomSplitOp::create(
1025 builder, loc, TypeRange{rng.getType(), rng.getType()}, rngMomentum);
1026 rngForMomentum = momSplit.getResult(0);
1027 }
1028 auto [p0, rngAfterMomentum] =
1029 sampleMomentum(builder, loc, rngForMomentum, ctx.invMass,
1030 ctx.massMatrixSqrt, positionType, debugDump);
1031
1032 // 3. Compute K0 = 0.5 * p^T * M^-1 * p
1033 auto K0 = computeKineticEnergy(builder, loc, p0, ctx.invMass, positionType);
1034
1035 // 4. Compute H0 = U + K0
1036 auto H0 = arith::AddFOp::create(builder, loc, U, K0);
1037
1038 // 5. Initialize NUTS tree state
1039 auto iterCtx = ctx.withH0(H0);
1040
1041 auto zeroI64 = arith::ConstantOp::create(
1042 builder, loc, i64TensorType,
1043 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
1044 auto falseConst = arith::ConstantOp::create(
1045 builder, loc, i1TensorType,
1046 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(false)));
1047 auto zeroConst = arith::ConstantOp::create(
1048 builder, loc, scalarType,
1049 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.0)));
1050
1051 NUTSTreeState initialTree = {.q_left = q,
1052 .p_left = p0,
1053 .grad_left = grad,
1054 .q_right = q,
1055 .p_right = p0,
1056 .grad_right = grad,
1057 .q_proposal = q,
1058 .grad_proposal = grad,
1059 .U_proposal = U,
1060 .H_proposal = H0,
1061 .depth = zeroI64,
1062 .weight = zeroConst,
1063 .turning = falseConst,
1064 .diverging = falseConst,
1065 .sum_accept_probs = zeroConst,
1066 .num_proposals = zeroI64,
1067 .p_sum = p0,
1068 .rng = rngTree};
1069
1070 // 6. Build NUTS tree
1071 auto finalTree = buildTree(builder, loc, initialTree, iterCtx, debugDump);
1072
1073 // 7. NUTS always accepts the proposal (implicit acceptance in the tree)
1074 auto trueConst = arith::ConstantOp::create(
1075 builder, loc, i1TensorType,
1076 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(true)));
1077
1078 // 8. Compute mean acceptance probability for step size adaptation
1079 auto oneI64 = arith::ConstantOp::create(
1080 builder, loc, i64TensorType,
1081 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1082 auto numProposalsClamped =
1083 arith::MaxSIOp::create(builder, loc, finalTree.num_proposals, oneI64);
1084 auto numProposalsFloat =
1085 arith::SIToFPOp::create(builder, loc, scalarType, numProposalsClamped);
1086 auto meanAcceptProb = arith::DivFOp::create(
1087 builder, loc, finalTree.sum_accept_probs, numProposalsFloat);
1088
1089 return {finalTree.q_proposal,
1090 finalTree.grad_proposal,
1091 finalTree.U_proposal,
1092 trueConst,
1093 finalTree.diverging,
1094 meanAcceptProb,
1095 rngNext};
1096}
1097
1098NUTSTreeState impulse::buildBaseTree(OpBuilder &builder, Location loc,
1099 const IntegratorState &leaf, Value rng,
1100 Value direction, const NUTSContext &ctx) {
1101 auto positionType = ctx.getPositionType();
1102 auto scalarType = ctx.getScalarType();
1103 auto elemType = ctx.getElementType();
1104 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1105 auto i1TensorType = RankedTensorType::get({}, builder.getI1Type());
1106
1107 auto oneConst = arith::ConstantOp::create(
1108 builder, loc, scalarType,
1109 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 1.0)));
1110 auto zeroI64 = arith::ConstantOp::create(
1111 builder, loc, i64TensorType,
1112 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
1113 auto oneI64 = arith::ConstantOp::create(
1114 builder, loc, i64TensorType,
1115 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1116 auto falseConst = arith::ConstantOp::create(
1117 builder, loc, i1TensorType,
1118 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(false)));
1119
1120 IntegrationResult leap =
1121 computeIntegrationStep(builder, loc, leaf, rng, direction, ctx);
1122
1123 auto qNew = leap.q;
1124 auto pNew = leap.p;
1125 auto gradNew = leap.grad;
1126 auto UNew = leap.U;
1127 auto rngOut = leap.rng;
1128
1129 auto KNew =
1130 computeKineticEnergy(builder, loc, pNew, ctx.invMass, positionType);
1131 auto HNew = arith::AddFOp::create(builder, loc, UNew, KNew);
1132 Value deltaH = arith::SubFOp::create(builder, loc, HNew, ctx.H0);
1133
1134 // NaN check
1135 auto isNan = arith::CmpFOp::create(builder, loc, arith::CmpFPredicate::UNE,
1136 deltaH, deltaH);
1137 auto infConst = arith::ConstantOp::create(
1138 builder, loc, scalarType,
1139 DenseElementsAttr::get(
1140 scalarType, builder.getFloatAttr(
1141 elemType, std::numeric_limits<double>::infinity())));
1142 deltaH = arith::SelectOp::create(builder, loc, scalarType, isNan, infConst,
1143 deltaH);
1144
1145 auto treeWeight = arith::NegFOp::create(builder, loc, deltaH);
1146
1147 // Check for divergence
1148 auto diverging = arith::CmpFOp::create(
1149 builder, loc, arith::CmpFPredicate::OGT, deltaH, ctx.maxDeltaEnergy);
1150
1151 auto negDeltaH = arith::NegFOp::create(builder, loc, deltaH);
1152 auto expNegDelta = math::ExpOp::create(builder, loc, negDeltaH);
1153 auto acceptProb =
1154 arith::MinimumFOp::create(builder, loc, oneConst, expNegDelta);
1155
1156 return {.q_left = qNew,
1157 .p_left = pNew,
1158 .grad_left = gradNew,
1159 .q_right = qNew,
1160 .p_right = pNew,
1161 .grad_right = gradNew,
1162 .q_proposal = qNew,
1163 .grad_proposal = gradNew,
1164 .U_proposal = UNew,
1165 .H_proposal = HNew,
1166 .depth = zeroI64,
1167 .weight = treeWeight,
1168 .turning = falseConst,
1169 .diverging = diverging,
1170 .sum_accept_probs = acceptProb,
1171 .num_proposals = oneI64,
1172 .p_sum = pNew,
1173 .rng = rngOut};
1174}
1175
1176IntegratorState impulse::getLeafFromTree(OpBuilder &builder, Location loc,
1177 const NUTSTreeState &tree,
1178 Value direction,
1179 const NUTSContext &ctx) {
1180 auto positionType = ctx.getPositionType();
1181
1182 auto leafQ = impulse::SelectOp::create(builder, loc, positionType, direction,
1183 tree.q_right, tree.q_left);
1184 auto leafP = impulse::SelectOp::create(builder, loc, positionType, direction,
1185 tree.p_right, tree.p_left);
1186 auto leafGrad = impulse::SelectOp::create(
1187 builder, loc, positionType, direction, tree.grad_right, tree.grad_left);
1188 return {leafQ, leafP, leafGrad};
1189}
1190
1192impulse::buildIterativeSubtree(OpBuilder &builder, Location loc,
1193 const NUTSTreeState &initialTree,
1194 Value direction, Value pCkpts, Value pSumCkpts,
1195 const NUTSContext &ctx, bool debugDump) {
1196 auto i1TensorType = RankedTensorType::get({}, builder.getI1Type());
1197 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1198 auto pCkptsType = cast<RankedTensorType>(pCkpts.getType());
1199 auto trueConst = arith::ConstantOp::create(
1200 builder, loc, i1TensorType,
1201 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(true)));
1202 auto oneI64 = arith::ConstantOp::create(
1203 builder, loc, i64TensorType,
1204 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1205 auto zeroI64 = arith::ConstantOp::create(
1206 builder, loc, i64TensorType,
1207 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
1208
1209 // 2 ^ (initialTree.depth)
1210 auto maxNumProposals =
1211 arith::ShLIOp::create(builder, loc, oneI64, initialTree.depth);
1212
1213 SmallVector<Type> whileTypes = initialTree.getTypes();
1214 whileTypes.push_back(pCkptsType);
1215 whileTypes.push_back(pCkptsType);
1216 whileTypes.push_back(i64TensorType);
1217
1218 SmallVector<Value> whileInitVals = initialTree.toValues();
1219 whileInitVals[15] = zeroI64; // zero `num_proposals`
1220 whileInitVals.push_back(pCkpts);
1221 whileInitVals.push_back(pSumCkpts);
1222 whileInitVals.push_back(zeroI64);
1223
1224 auto whileOp =
1225 impulse::WhileOp::create(builder, loc, whileTypes, whileInitVals);
1226
1227 // Check: num_proposals < max_num_proposals && !turning && !diverging
1228 Block *condBlock = builder.createBlock(&whileOp.getConditionRegion());
1229 for (auto type : whileTypes)
1230 condBlock->addArgument(type, loc);
1231
1232 builder.setInsertionPointToStart(condBlock);
1233 SmallVector<Value> condTreeArgs(condBlock->getArguments().begin(),
1234 condBlock->getArguments().begin() + 18);
1235 NUTSTreeState condTree = NUTSTreeState::fromValues(condTreeArgs);
1236
1237 auto numProposalsCheck =
1238 arith::CmpIOp::create(builder, loc, arith::CmpIPredicate::slt,
1239 condTree.num_proposals, maxNumProposals);
1240 auto notTurning =
1241 arith::XOrIOp::create(builder, loc, condTree.turning, trueConst);
1242 auto notDiverging =
1243 arith::XOrIOp::create(builder, loc, condTree.diverging, trueConst);
1244 auto continueCond = arith::AndIOp::create(
1245 builder, loc,
1246 arith::AndIOp::create(builder, loc, numProposalsCheck, notTurning),
1247 notDiverging);
1248
1249 // Yield continue condition
1250 impulse::YieldOp::create(builder, loc, ValueRange{continueCond});
1251
1252 Block *bodyBlock = builder.createBlock(&whileOp.getBodyRegion());
1253 for (auto type : whileTypes)
1254 bodyBlock->addArgument(type, loc);
1255
1256 builder.setInsertionPointToStart(bodyBlock);
1257
1258 SmallVector<Value> bodyTreeArgs(bodyBlock->getArguments().begin(),
1259 bodyBlock->getArguments().begin() + 18);
1260 NUTSTreeState bodyTree = NUTSTreeState::fromValues(bodyTreeArgs);
1261 auto bodyPCkpts = bodyBlock->getArgument(18);
1262 auto bodyPSumCkpts = bodyBlock->getArgument(19);
1263 auto bodyLeafIdx = bodyBlock->getArgument(20);
1264
1265 // Extract leaf based on direction
1266 IntegratorState leaf =
1267 getLeafFromTree(builder, loc, bodyTree, direction, ctx);
1268
1269 auto rngSplit2 = impulse::RandomSplitOp::create(
1270 builder, loc, TypeRange{bodyTree.rng.getType(), bodyTree.rng.getType()},
1271 bodyTree.rng);
1272 auto rngNext = rngSplit2.getResult(0);
1273 auto rngCombine = rngSplit2.getResult(1);
1274
1275 // Build base tree
1276 NUTSTreeState newLeaf =
1277 buildBaseTree(builder, loc, leaf, rngNext, direction, ctx);
1278
1279 // First leaf check
1280 auto isFirstLeaf = arith::CmpIOp::create(
1281 builder, loc, arith::CmpIPredicate::eq, bodyTree.num_proposals, zeroI64);
1282
1283 SmallVector<Type> treeTypes = newLeaf.getTypes();
1284 auto ifOp = impulse::IfOp::create(builder, loc, treeTypes, isFirstLeaf);
1285 {
1286 Block *trueBranch = builder.createBlock(&ifOp.getTrueBranch());
1287 builder.setInsertionPointToStart(trueBranch);
1288 impulse::YieldOp::create(builder, loc, newLeaf.toValues());
1289 }
1290 {
1291 Block *falseBranch = builder.createBlock(&ifOp.getFalseBranch());
1292 builder.setInsertionPointToStart(falseBranch);
1293 NUTSTreeState combinedTree =
1294 combineTrees(builder, loc, bodyTree, newLeaf, direction, rngCombine,
1295 /*biased=*/false, ctx);
1296 impulse::YieldOp::create(builder, loc, combinedTree.toValues());
1297 }
1298
1299 builder.setInsertionPointAfter(ifOp);
1301 SmallVector<Value>(ifOp.getResults().begin(), ifOp.getResults().end()));
1302 updatedTree.rng = rngNext;
1303
1304 // Update and check iterative turning
1305 auto [ckptIdxMin, ckptIdxMax] =
1306 leafIdxToCheckpointIdxs(builder, loc, bodyLeafIdx);
1307 auto [updatedPCkpts, updatedPSumCkpts] = updateCheckpoints(
1308 builder, loc, bodyLeafIdx, ckptIdxMax, newLeaf.p_right, updatedTree.p_sum,
1309 bodyPCkpts, bodyPSumCkpts, ctx, debugDump);
1310 auto iterativeTurning = checkIterativeTurning(
1311 builder, loc, newLeaf.p_right, updatedTree.p_sum, updatedPCkpts,
1312 updatedPSumCkpts, ckptIdxMin, ckptIdxMax, ctx, debugDump);
1313
1314 updatedTree.turning =
1315 impulse::SelectOp::create(builder, loc, i1TensorType, isFirstLeaf,
1316 newLeaf.turning, iterativeTurning);
1317
1318 auto nextLeafIdx = arith::AddIOp::create(builder, loc, bodyLeafIdx, oneI64);
1319
1320 SmallVector<Value> yieldVals = updatedTree.toValues();
1321 yieldVals.push_back(updatedPCkpts);
1322 yieldVals.push_back(updatedPSumCkpts);
1323 yieldVals.push_back(nextLeafIdx);
1324 impulse::YieldOp::create(builder, loc, yieldVals);
1325
1326 builder.setInsertionPointAfter(whileOp);
1327
1328 SmallVector<Value> resultTreeArgs(whileOp.getResults().begin(),
1329 whileOp.getResults().begin() + 18);
1330 NUTSTreeState resultTree = NUTSTreeState::fromValues(resultTreeArgs);
1331 auto resultPCkpts = whileOp.getResult(18);
1332 auto resultPSumCkpts = whileOp.getResult(19);
1333
1334 // `combineTrees` increments depth at each leaf building step; we need to
1335 // restore to target depth here
1336 resultTree.depth = initialTree.depth;
1337
1338 return {resultTree, resultPCkpts, resultPSumCkpts};
1339}
1340
1341SubtreeBuildResult impulse::doubleTree(OpBuilder &builder, Location loc,
1342 const NUTSTreeState &tree,
1343 Value direction, Value pCkpts,
1344 Value pSumCkpts, const NUTSContext &ctx,
1345 bool debugDump) {
1346 auto rngSplit2 = impulse::RandomSplitOp::create(
1347 builder, loc, TypeRange{tree.rng.getType(), tree.rng.getType()},
1348 tree.rng);
1349 auto rngSubtree = rngSplit2.getResult(0);
1350 auto rngTransition = rngSplit2.getResult(1);
1351
1352 NUTSTreeState subTreeInit = tree;
1353 subTreeInit.rng = rngSubtree;
1354 auto subtreeResult = buildIterativeSubtree(
1355 builder, loc, subTreeInit, direction, pCkpts, pSumCkpts, ctx, debugDump);
1356
1357 // Tree combine using *biased* transition kernel
1358 NUTSTreeState combinedTree =
1359 combineTrees(builder, loc, tree, subtreeResult.tree, direction,
1360 rngTransition, /*biased=*/true, ctx);
1361
1362 return {combinedTree, subtreeResult.pCkpts, subtreeResult.pSumCkpts};
1363}
1364
1365NUTSTreeState impulse::buildTree(OpBuilder &builder, Location loc,
1366 const NUTSTreeState &initialTree,
1367 const NUTSContext &ctx, bool debugDump) {
1368 auto elemType =
1369 cast<RankedTensorType>(ctx.stepSize.getType()).getElementType();
1370 auto F64TensorType = RankedTensorType::get({}, elemType);
1371 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1372 auto i1TensorType = RankedTensorType::get({}, builder.getI1Type());
1373
1374 auto trueConst = arith::ConstantOp::create(
1375 builder, loc, i1TensorType,
1376 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(true)));
1377 auto halfConst = arith::ConstantOp::create(
1378 builder, loc, F64TensorType,
1379 DenseElementsAttr::get(F64TensorType, builder.getF64FloatAttr(0.5)));
1380 auto zeroConst = arith::ConstantOp::create(
1381 builder, loc, F64TensorType,
1382 DenseElementsAttr::get(F64TensorType, builder.getF64FloatAttr(0.0)));
1383 auto oneConst = arith::ConstantOp::create(
1384 builder, loc, F64TensorType,
1385 DenseElementsAttr::get(F64TensorType, builder.getF64FloatAttr(1.0)));
1386
1387 auto maxTreeDepth = arith::ConstantOp::create(
1388 builder, loc, i64TensorType,
1389 DenseElementsAttr::get(i64TensorType,
1390 builder.getI64IntegerAttr(ctx.maxTreeDepth)));
1391
1392 auto checkpointType =
1393 RankedTensorType::get({ctx.maxTreeDepth, ctx.positionSize}, elemType);
1394
1395 SmallVector<Type> whileTypes = initialTree.getTypes();
1396 SmallVector<Value> whileInitVals = initialTree.toValues();
1397
1398 auto whileOp =
1399 impulse::WhileOp::create(builder, loc, whileTypes, whileInitVals);
1400
1401 // Check: (depth < maxTreeDepth) && !turning && !diverging
1402 Block *condBlock = builder.createBlock(&whileOp.getConditionRegion());
1403 for (auto type : whileTypes)
1404 condBlock->addArgument(type, loc);
1405
1406 builder.setInsertionPointToStart(condBlock);
1407
1408 SmallVector<Value> condArgs(condBlock->getArguments().begin(),
1409 condBlock->getArguments().end());
1410 NUTSTreeState condTree = NUTSTreeState::fromValues(condArgs);
1411
1412 auto depthCheck = arith::CmpIOp::create(
1413 builder, loc, arith::CmpIPredicate::slt, condTree.depth, maxTreeDepth);
1414 auto notTurning =
1415 arith::XOrIOp::create(builder, loc, condTree.turning, trueConst);
1416 auto notDiverging =
1417 arith::XOrIOp::create(builder, loc, condTree.diverging, trueConst);
1418
1419 // Yield continue condition
1420 auto continueCond = arith::AndIOp::create(
1421 builder, loc, arith::AndIOp::create(builder, loc, depthCheck, notTurning),
1422 notDiverging);
1423
1424 impulse::YieldOp::create(builder, loc, ValueRange{continueCond});
1425
1426 Block *bodyBlock = builder.createBlock(&whileOp.getBodyRegion());
1427 for (auto type : whileTypes)
1428 bodyBlock->addArgument(type, loc);
1429
1430 builder.setInsertionPointToStart(bodyBlock);
1431
1432 SmallVector<Value> bodyArgs(bodyBlock->getArguments().begin(),
1433 bodyBlock->getArguments().end());
1434 NUTSTreeState bodyTree = NUTSTreeState::fromValues(bodyArgs);
1435
1436 // Create fresh checkpoint tensors
1437 auto zeroCkpts = arith::ConstantOp::create(
1438 builder, loc, checkpointType,
1439 DenseElementsAttr::get(checkpointType, builder.getF64FloatAttr(0.0)));
1440 Value bodyPCkpts = zeroCkpts;
1441 Value bodyPSumCkpts = zeroCkpts;
1442
1443 auto rngSplit3 = impulse::RandomSplitOp::create(
1444 builder, loc,
1445 TypeRange{bodyTree.rng.getType(), bodyTree.rng.getType(),
1446 bodyTree.rng.getType()},
1447 bodyTree.rng);
1448 auto rngNext = rngSplit3.getResult(0);
1449 auto rngDir = rngSplit3.getResult(1);
1450 auto rngDbl = rngSplit3.getResult(2);
1451
1452 auto directionRandom = impulse::RandomOp::create(
1453 builder, loc, TypeRange{rngDir.getType(), F64TensorType}, rngDir,
1454 zeroConst, oneConst,
1455 impulse::RngDistributionAttr::get(builder.getContext(),
1456 impulse::RngDistribution::UNIFORM));
1457 auto direction =
1458 arith::CmpFOp::create(builder, loc, arith::CmpFPredicate::OLT,
1459 directionRandom.getResult(), halfConst);
1460
1461 // Double the tree
1462 NUTSTreeState treeToDouble = bodyTree;
1463 treeToDouble.rng = rngDbl;
1464 auto doubleResult = doubleTree(builder, loc, treeToDouble, direction,
1465 bodyPCkpts, bodyPSumCkpts, ctx, debugDump);
1466
1467 NUTSTreeState treeToYield = doubleResult.tree;
1468 treeToYield.rng = rngNext;
1469 impulse::YieldOp::create(builder, loc, treeToYield.toValues());
1470
1471 builder.setInsertionPointAfter(whileOp);
1472
1473 SmallVector<Value> results(whileOp.getResults().begin(),
1474 whileOp.getResults().end());
1475 return NUTSTreeState::fromValues(results);
1476}
1477
1478std::pair<Value, Value> impulse::leafIdxToCheckpointIdxs(OpBuilder &builder,
1479 Location loc,
1480 Value leafIdx) {
1481 auto i64TensorType = cast<RankedTensorType>(leafIdx.getType());
1482
1483 auto oneConst = arith::ConstantOp::create(
1484 builder, loc, i64TensorType,
1485 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1486
1487 // idx_max = popcount(leafIdx >> 1)
1488 auto shiftedIdx = arith::ShRUIOp::create(builder, loc, leafIdx, oneConst);
1489 auto idxMax =
1490 impulse::PopcountOp::create(builder, loc, i64TensorType, shiftedIdx);
1491
1492 // num_subtrees = popcount((~leafIdx & (leafIdx + 1)) - 1)
1493 auto leafIdxPlusOne = arith::AddIOp::create(builder, loc, leafIdx, oneConst);
1494
1495 auto minusOneConst = arith::ConstantOp::create(
1496 builder, loc, i64TensorType,
1497 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(-1)));
1498 auto notLeafIdx = arith::XOrIOp::create(builder, loc, leafIdx, minusOneConst);
1499
1500 Value andResult =
1501 arith::AndIOp::create(builder, loc, notLeafIdx, leafIdxPlusOne);
1502 Value andMinusOne = arith::SubIOp::create(builder, loc, andResult, oneConst);
1503 Value numSubtrees =
1504 impulse::PopcountOp::create(builder, loc, i64TensorType, andMinusOne);
1505
1506 // idx_min = idx_max - num_subtrees + 1
1507 Value idxMaxMinusNumSubtrees =
1508 arith::SubIOp::create(builder, loc, idxMax, numSubtrees);
1509 Value idxMin =
1510 arith::AddIOp::create(builder, loc, idxMaxMinusNumSubtrees, oneConst);
1511
1512 return {idxMin, idxMax};
1513}
1514
1515Value impulse::checkIterativeTurning(OpBuilder &builder, Location loc, Value p,
1516 Value pSum, Value pCkpts, Value pSumCkpts,
1517 Value idxMin, Value idxMax,
1518 const NUTSContext &ctx, bool debugDump) {
1519 auto positionType = ctx.getPositionType();
1520 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1521 auto i1TensorType = RankedTensorType::get({}, builder.getI1Type());
1522
1523 auto falseConst = arith::ConstantOp::create(
1524 builder, loc, i1TensorType,
1525 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(false)));
1526 auto oneI64 = arith::ConstantOp::create(
1527 builder, loc, i64TensorType,
1528 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1529
1530 // Iterate from `idx_max` down to `idx_min`, check turning at each checkpoint
1531 SmallVector<Type> whileTypes = {i64TensorType, i1TensorType};
1532 SmallVector<Value> whileInitVals = {idxMax, falseConst};
1533
1534 auto whileOp =
1535 impulse::WhileOp::create(builder, loc, whileTypes, whileInitVals);
1536 Block *condBlock = builder.createBlock(&whileOp.getConditionRegion());
1537 condBlock->addArgument(i64TensorType, loc);
1538 condBlock->addArgument(i1TensorType, loc);
1539 builder.setInsertionPointToStart(condBlock);
1540
1541 Value iCond = condBlock->getArgument(0);
1542 Value turningCond = condBlock->getArgument(1);
1543
1544 auto iGeMin = arith::CmpIOp::create(builder, loc, arith::CmpIPredicate::sge,
1545 iCond, idxMin);
1546 auto trueConst = arith::ConstantOp::create(
1547 builder, loc, i1TensorType,
1548 DenseElementsAttr::get(i1TensorType, builder.getBoolAttr(true)));
1549 auto notTurning = arith::XOrIOp::create(builder, loc, turningCond, trueConst);
1550 auto continueLoop = arith::AndIOp::create(builder, loc, iGeMin, notTurning);
1551
1552 impulse::YieldOp::create(builder, loc, ValueRange{continueLoop.getResult()});
1553
1554 Block *bodyBlock = builder.createBlock(&whileOp.getBodyRegion());
1555 bodyBlock->addArgument(i64TensorType, loc);
1556 bodyBlock->addArgument(i1TensorType, loc);
1557 builder.setInsertionPointToStart(bodyBlock);
1558 Value iBody = bodyBlock->getArgument(0);
1559
1560 auto zeroI64 = arith::ConstantOp::create(
1561 builder, loc, i64TensorType,
1562 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
1563 Value pLeft = impulse::DynamicSliceOp::create(
1564 builder, loc, positionType, pCkpts, ValueRange{iBody, zeroI64},
1565 builder.getDenseI64ArrayAttr({1, ctx.positionSize}));
1566 Value pSumCkptI = impulse::DynamicSliceOp::create(
1567 builder, loc, positionType, pSumCkpts, ValueRange{iBody, zeroI64},
1568 builder.getDenseI64ArrayAttr({1, ctx.positionSize}));
1569
1570 // Compute subtree momentum sum: pSum - pSumCkpts[i] + pCkpts[i]
1571 auto pSumMinusCkpt = arith::SubFOp::create(builder, loc, pSum, pSumCkptI);
1572 Value subtreePSum = arith::AddFOp::create(builder, loc, pSumMinusCkpt, pLeft);
1573
1574 // Check turning
1575 Value turningAtCkpt = checkTurning(builder, loc, pLeft, p, subtreePSum, ctx);
1576
1577 Value iNext = arith::SubIOp::create(builder, loc, iBody, oneI64);
1578 impulse::YieldOp::create(builder, loc, ValueRange{iNext, turningAtCkpt});
1579
1580 builder.setInsertionPointAfter(whileOp);
1581 return whileOp.getResult(1);
1582}
1583
1584std::pair<Value, Value>
1585impulse::updateCheckpoints(OpBuilder &builder, Location loc, Value leafIdx,
1586 Value ckptIdxMax, Value p, Value pSum, Value pCkpts,
1587 Value pSumCkpts, const NUTSContext &ctx,
1588 bool debugDump) {
1589 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1590 auto oneI64 = arith::ConstantOp::create(
1591 builder, loc, i64TensorType,
1592 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1593 auto zeroI64 = arith::ConstantOp::create(
1594 builder, loc, i64TensorType,
1595 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
1596
1597 Value leafIdxBit0 = arith::AndIOp::create(builder, loc, leafIdx, oneI64);
1598 Value isEven = arith::CmpIOp::create(builder, loc, arith::CmpIPredicate::eq,
1599 leafIdxBit0, zeroI64);
1600
1601 auto pCkptsType = cast<RankedTensorType>(pCkpts.getType());
1602
1603 // Compute updates only on even leafIdx
1604 SmallVector<Type> ifResultTypes = {pCkptsType, pCkptsType};
1605 auto ifOp = impulse::IfOp::create(builder, loc, ifResultTypes, isEven);
1606 {
1607 Block *trueBranch = builder.createBlock(&ifOp.getTrueBranch());
1608 builder.setInsertionPointToStart(trueBranch);
1609
1610 auto updatedPCkpts = impulse::DynamicUpdateSliceOp::create(
1611 builder, loc, pCkptsType, pCkpts, p, ValueRange{ckptIdxMax, zeroI64});
1612 auto updatedPSumCkpts = impulse::DynamicUpdateSliceOp::create(
1613 builder, loc, pCkptsType, pSumCkpts, pSum,
1614 ValueRange{ckptIdxMax, zeroI64});
1615
1616 impulse::YieldOp::create(builder, loc,
1617 ValueRange{updatedPCkpts, updatedPSumCkpts});
1618 }
1619 {
1620 Block *falseBranch = builder.createBlock(&ifOp.getFalseBranch());
1621 builder.setInsertionPointToStart(falseBranch);
1622
1623 impulse::YieldOp::create(builder, loc, ValueRange{pCkpts, pSumCkpts});
1624 }
1625
1626 builder.setInsertionPointAfter(ifOp);
1627
1628 Value finalPCkpts = ifOp.getResult(0);
1629 Value finalPSumCkpts = ifOp.getResult(1);
1630
1631 return {finalPCkpts, finalPSumCkpts};
1632}
1633
1634DualAveragingState impulse::initDualAveraging(OpBuilder &builder, Location loc,
1635 Value stepSize,
1636 bool logOfProduct) {
1637 auto stepSizeType = cast<RankedTensorType>(stepSize.getType());
1638 auto elemType = stepSizeType.getElementType();
1639 auto scalarType = RankedTensorType::get({}, elemType);
1640 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1641
1642 // prox_center: log(10 * step_size) at warmup start, or log(10) +
1643 // log(step_size) at window-end re-inits
1644 Value proxCenter;
1645 if (logOfProduct) {
1646 auto tenConst = arith::ConstantOp::create(
1647 builder, loc, scalarType,
1648 DenseElementsAttr::get(scalarType,
1649 builder.getFloatAttr(elemType, 10.0)));
1650 Value scaled = arith::MulFOp::create(builder, loc, tenConst, stepSize);
1651 proxCenter = math::LogOp::create(builder, loc, scaled);
1652 } else {
1653 auto log10Const = arith::ConstantOp::create(
1654 builder, loc, scalarType,
1655 DenseElementsAttr::get(scalarType,
1656 builder.getFloatAttr(elemType, std::log(10.0))));
1657 Value logStepSize = math::LogOp::create(builder, loc, stepSize);
1658 proxCenter = arith::AddFOp::create(builder, loc, log10Const, logStepSize);
1659 }
1660
1661 auto zeroConst = arith::ConstantOp::create(
1662 builder, loc, scalarType,
1663 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.0)));
1664 auto zeroI64 = arith::ConstantOp::create(
1665 builder, loc, i64TensorType,
1666 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
1667
1668 return {
1669 .log_step_size = zeroConst,
1670 .log_step_size_avg = zeroConst,
1671 .gradient_avg = zeroConst,
1672 .step_count = zeroI64,
1673 .prox_center = proxCenter,
1674 };
1675}
1676
1678impulse::updateDualAveraging(OpBuilder &builder, Location loc,
1679 const DualAveragingState &state, Value acceptProb,
1680 const DualAveragingConfig &config) {
1681 // Dual Averaging update:
1682 // g = target_accept_prob - accept_prob
1683 // g_avg = (1 - 1/(t+t0)) * g_avg + g/(t+t0)
1684 // log_step_size = prox_center - sqrt(t)/gamma * g_avg
1685 // log_step_size_avg = (1 - t^(-kappa)) * log_step_size_avg +
1686 // t^(-kappa) * log_step_size
1687 auto acceptProbType = cast<RankedTensorType>(acceptProb.getType());
1688 auto elemType = acceptProbType.getElementType();
1689 auto scalarType = RankedTensorType::get({}, elemType);
1690 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1691
1692 // t = t + 1
1693 auto oneI64 = arith::ConstantOp::create(
1694 builder, loc, i64TensorType,
1695 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1696 Value tNew = arith::AddIOp::create(builder, loc, state.step_count, oneI64);
1697 Value tFloat = arith::SIToFPOp::create(builder, loc, scalarType, tNew);
1698
1699 // t0
1700 auto t0Const = arith::ConstantOp::create(
1701 builder, loc, scalarType,
1702 DenseElementsAttr::get(scalarType,
1703 builder.getFloatAttr(elemType, config.t0)));
1704
1705 // g = target_accept_prob - accept_prob
1706 auto targetConst = arith::ConstantOp::create(
1707 builder, loc, scalarType,
1708 DenseElementsAttr::get(
1709 scalarType,
1710 builder.getFloatAttr(elemType, config.target_accept_prob)));
1711 Value g = arith::SubFOp::create(builder, loc, targetConst, acceptProb);
1712
1713 // t_plus_t0 = t + t0
1714 Value tPlusT0 = arith::AddFOp::create(builder, loc, tFloat, t0Const);
1715
1716 // weight = 1 / (t + t0)
1717 auto oneConst = arith::ConstantOp::create(
1718 builder, loc, scalarType,
1719 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 1.0)));
1720 Value weight = arith::DivFOp::create(builder, loc, oneConst, tPlusT0);
1721
1722 // decay = 1 - weight = 1 - 1/(t + t0)
1723 Value decay = arith::SubFOp::create(builder, loc, oneConst, weight);
1724
1725 // g_avg = decay * g_avg + weight * g
1726 // g_avg = (1 - 1/(t+t0)) * g_avg + g/(t+t0)
1727 Value gAvgDecayed =
1728 arith::MulFOp::create(builder, loc, decay, state.gradient_avg);
1729 Value gWeighted = arith::MulFOp::create(builder, loc, weight, g);
1730 Value gAvgNew = arith::AddFOp::create(builder, loc, gAvgDecayed, gWeighted);
1731
1732 // x_t = prox_center - sqrt(t) / gamma * g_avg
1733 Value sqrtT = math::SqrtOp::create(builder, loc, tFloat);
1734 auto gammaConst = arith::ConstantOp::create(
1735 builder, loc, scalarType,
1736 DenseElementsAttr::get(scalarType,
1737 builder.getFloatAttr(elemType, config.gamma)));
1738 Value sqrtTOverGamma = arith::DivFOp::create(builder, loc, sqrtT, gammaConst);
1739 Value adjustment =
1740 arith::MulFOp::create(builder, loc, sqrtTOverGamma, gAvgNew);
1741 Value logStepSizeNew =
1742 arith::SubFOp::create(builder, loc, state.prox_center, adjustment);
1743
1744 // weight_t = t^(-kappa)
1745 auto negKappaConst = arith::ConstantOp::create(
1746 builder, loc, scalarType,
1747 DenseElementsAttr::get(scalarType,
1748 builder.getFloatAttr(elemType, -config.kappa)));
1749 Value weightT = math::PowFOp::create(builder, loc, tFloat, negKappaConst);
1750
1751 // x_avg = (1 - weight_t) * x_avg + weight_t * x_t
1752 Value oneMinusWeightT =
1753 arith::SubFOp::create(builder, loc, oneConst, weightT);
1754 Value avgDecayed = arith::MulFOp::create(builder, loc, oneMinusWeightT,
1755 state.log_step_size_avg);
1756 Value newContribution =
1757 arith::MulFOp::create(builder, loc, weightT, logStepSizeNew);
1758 Value logStepSizeAvgNew =
1759 arith::AddFOp::create(builder, loc, avgDecayed, newContribution);
1760
1761 return {
1762 .log_step_size = logStepSizeNew,
1763 .log_step_size_avg = logStepSizeAvgNew,
1764 .gradient_avg = gAvgNew,
1765 .step_count = tNew,
1766 .prox_center = state.prox_center,
1767 };
1768}
1769
1770Value impulse::getStepSizeFromDualAveraging(OpBuilder &builder, Location loc,
1771 const DualAveragingState &state,
1772 bool final) {
1773 Value logStepSize = final ? state.log_step_size_avg : state.log_step_size;
1774 return math::ExpOp::create(builder, loc, logStepSize);
1775}
1776
1777WelfordState impulse::initWelford(OpBuilder &builder, Location loc,
1778 int64_t positionSize, bool diagonal) {
1779 auto elemType = builder.getF64Type();
1780 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1781 auto meanType = RankedTensorType::get({positionSize}, elemType);
1782
1783 Value mean = arith::ConstantOp::create(
1784 builder, loc, meanType,
1785 DenseElementsAttr::get(meanType, builder.getFloatAttr(elemType, 0.0)));
1786
1787 // Diagonal -> tensor<positionSize>
1788 // Dense -> tensor<positionSize x positionSize>
1789 Value m2;
1790 if (diagonal) {
1791 m2 = arith::ConstantOp::create(
1792 builder, loc, meanType,
1793 DenseElementsAttr::get(meanType, builder.getFloatAttr(elemType, 0.0)));
1794 } else {
1795 auto m2Type = RankedTensorType::get({positionSize, positionSize}, elemType);
1796 m2 = arith::ConstantOp::create(
1797 builder, loc, m2Type,
1798 DenseElementsAttr::get(m2Type, builder.getFloatAttr(elemType, 0.0)));
1799 }
1800
1801 Value n = arith::ConstantOp::create(
1802 builder, loc, i64TensorType,
1803 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(0)));
1804
1805 return {mean, m2, n};
1806}
1807
1808WelfordState impulse::updateWelford(OpBuilder &builder, Location loc,
1809 const WelfordState &state, Value sample,
1810 const WelfordConfig &config) {
1811 // Algorithm:
1812 // n = n + 1
1813 // delta_pre = sample - mean
1814 // mean = mean + delta_pre / n
1815 // delta_post = sample - mean
1816 // (if diagonal) m2 = m2 + delta_pre * delta_post
1817 // (if dense) m2 = m2 + outer(delta_post, delta_pre)
1818
1819 auto sampleType = cast<RankedTensorType>(sample.getType());
1820 auto elemType = sampleType.getElementType();
1821 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1822
1823 auto oneI64 = arith::ConstantOp::create(
1824 builder, loc, i64TensorType,
1825 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1826 Value nNew = arith::AddIOp::create(builder, loc, state.n, oneI64);
1827
1828 auto scalarType = RankedTensorType::get({}, elemType);
1829 Value nFloat = arith::SIToFPOp::create(builder, loc, scalarType, nNew);
1830
1831 Value nBroadcast = BroadcastOp::create(builder, loc, sampleType, nFloat,
1832 sampleType.getShape());
1833
1834 Value deltaPre = arith::SubFOp::create(builder, loc, sample, state.mean);
1835
1836 Value deltaPreOverN =
1837 arith::DivFOp::create(builder, loc, deltaPre, nBroadcast);
1838 Value meanNew =
1839 arith::AddFOp::create(builder, loc, state.mean, deltaPreOverN);
1840
1841 Value deltaPost = arith::SubFOp::create(builder, loc, sample, meanNew);
1842
1843 Value m2New;
1844 if (config.diagonal) {
1845 Value product = arith::MulFOp::create(builder, loc, deltaPre, deltaPost);
1846 m2New = arith::AddFOp::create(builder, loc, state.m2, product);
1847 } else { // Dense
1848 auto m2Type = cast<RankedTensorType>(state.m2.getType());
1849 Value outerProduct = impulse::DotOp::create(
1850 builder, loc, m2Type, deltaPost, deltaPre,
1851 /*lhs_batching_dimensions=*/builder.getDenseI64ArrayAttr({}),
1852 /*rhs_batching_dimensions=*/builder.getDenseI64ArrayAttr({}),
1853 /*lhs_contracting_dimensions=*/builder.getDenseI64ArrayAttr({}),
1854 /*rhs_contracting_dimensions=*/builder.getDenseI64ArrayAttr({}));
1855 m2New = arith::AddFOp::create(builder, loc, state.m2, outerProduct);
1856 }
1857
1858 return {meanNew, m2New, nNew};
1859}
1860
1861Value impulse::finalizeWelford(OpBuilder &builder, Location loc,
1862 const WelfordState &state,
1863 const WelfordConfig &config) {
1864 // Compute sample covariance: cov = m2 / (n - 1)
1865 auto m2Type = cast<RankedTensorType>(state.m2.getType());
1866 auto elemType = m2Type.getElementType();
1867 auto scalarType = RankedTensorType::get({}, elemType);
1868 auto i64TensorType = RankedTensorType::get({}, builder.getI64Type());
1869
1870 auto oneI64 = arith::ConstantOp::create(
1871 builder, loc, i64TensorType,
1872 DenseElementsAttr::get(i64TensorType, builder.getI64IntegerAttr(1)));
1873 Value nMinus1 = arith::SubIOp::create(builder, loc, state.n, oneI64);
1874 Value nMinus1Float =
1875 arith::SIToFPOp::create(builder, loc, scalarType, nMinus1);
1876
1877 Value nMinus1Bcast = BroadcastOp::create(builder, loc, m2Type, nMinus1Float,
1878 m2Type.getShape());
1879
1880 Value cov = arith::DivFOp::create(builder, loc, state.m2, nMinus1Bcast);
1881
1882 // (Optional) Regularization (Stan's shrinkage):
1883 // scaled_cov = (n / (n + 5)) * cov
1884 // shrinkage = 1e-3 * (5 / (n + 5))
1885 // (if diagonal) cov = scaled_cov + shrinkage
1886 // (if dense) cov = scaled_cov + shrinkage * I
1887 if (config.regularize) {
1888 Value nFloat = arith::SIToFPOp::create(builder, loc, scalarType, state.n);
1889
1890 auto fiveConst = arith::ConstantOp::create(
1891 builder, loc, scalarType,
1892 DenseElementsAttr::get(scalarType,
1893 builder.getFloatAttr(elemType, 5.0)));
1894 Value nPlusFive = arith::AddFOp::create(builder, loc, nFloat, fiveConst);
1895
1896 Value scale = arith::DivFOp::create(builder, loc, nFloat, nPlusFive);
1897 Value scaleBcast =
1898 BroadcastOp::create(builder, loc, m2Type, scale, m2Type.getShape());
1899 Value scaledCov = arith::MulFOp::create(builder, loc, scaleBcast, cov);
1900
1901 auto shrinkageBaseConst = arith::ConstantOp::create(
1902 builder, loc, scalarType,
1903 DenseElementsAttr::get(scalarType,
1904 builder.getFloatAttr(elemType, 1e-3 * 5.0)));
1905 Value shrinkage =
1906 arith::DivFOp::create(builder, loc, shrinkageBaseConst, nPlusFive);
1907
1908 if (config.diagonal) {
1909 Value shrinkageBcast = BroadcastOp::create(builder, loc, m2Type,
1910 shrinkage, m2Type.getShape());
1911 cov = arith::AddFOp::create(builder, loc, scaledCov, shrinkageBcast);
1912 } else {
1913 Value identity = createIdentityMatrix(builder, loc, m2Type);
1914 Value shrinkageBcast = BroadcastOp::create(builder, loc, m2Type,
1915 shrinkage, m2Type.getShape());
1916 Value shrinkageI =
1917 arith::MulFOp::create(builder, loc, shrinkageBcast, identity);
1918 cov = arith::AddFOp::create(builder, loc, scaledCov, shrinkageI);
1919 }
1920 }
1921
1922 return cov;
1923}
1924
1925SmallVector<AdaptWindow> impulse::buildAdaptationSchedule(int64_t numSteps) {
1926 // |<-- start buffer -->|<-- middle windows (doubling) -->|<-- end buffer -->|
1927 // | (no mass) | (collect + adapt mass) | (no mass) |
1928 // | step size only | step size + mass matrix | step size only |
1929
1930 SmallVector<AdaptWindow> schedule;
1931
1932 if (numSteps < 20) {
1933 schedule.push_back({0, numSteps - 1});
1934 return schedule;
1935 }
1936
1937 // Stan-style window schedule
1938 int64_t startBufferSize = 75;
1939 int64_t endBufferSize = 50;
1940 int64_t initWindowSize = 25;
1941
1942 if ((startBufferSize + endBufferSize + initWindowSize) > numSteps) {
1943 startBufferSize = static_cast<int64_t>(0.15 * numSteps);
1944 endBufferSize = static_cast<int64_t>(0.1 * numSteps);
1945 initWindowSize = numSteps - startBufferSize - endBufferSize;
1946 }
1947
1948 // Start buffer window
1949 schedule.push_back({0, startBufferSize - 1});
1950
1951 int64_t endWindowStart = numSteps - endBufferSize;
1952 int64_t nextWindowSize = initWindowSize;
1953 int64_t nextWindowStart = startBufferSize;
1954
1955 // Middle windows
1956 while (nextWindowStart < endWindowStart) {
1957 int64_t curWindowStart = nextWindowStart;
1958 int64_t curWindowSize = nextWindowSize;
1959
1960 if (3 * curWindowSize <= endWindowStart - curWindowStart) {
1961 nextWindowSize = 2 * curWindowSize;
1962 } else {
1963 curWindowSize = endWindowStart - curWindowStart;
1964 }
1965
1966 nextWindowStart = curWindowStart + curWindowSize;
1967 schedule.push_back({curWindowStart, nextWindowStart - 1});
1968 }
1969
1970 // End buffer window
1971 schedule.push_back({endWindowStart, numSteps - 1});
1972
1973 return schedule;
1974}
1975
1976Value impulse::unconstrainPosition(OpBuilder &builder, Location loc,
1977 Value constrained,
1978 ArrayRef<SupportInfo> supports) {
1979 bool hasConstraints = false;
1980 for (const auto &info : supports) {
1981 if (info.support && info.support.getKind() != impulse::SupportKind::REAL) {
1982 hasConstraints = true;
1983 break;
1984 }
1985 }
1986 if (!hasConstraints || supports.empty())
1987 return constrained;
1988
1989 auto inputType = cast<RankedTensorType>(constrained.getType());
1990 auto elemType = inputType.getElementType();
1991 int64_t positionSize = inputType.getShape()[1];
1992
1993 auto positionType1D = RankedTensorType::get({positionSize}, elemType);
1994 Value constrained1D =
1995 impulse::ReshapeOp::create(builder, loc, positionType1D, constrained);
1996
1997 SmallVector<Value> slices;
1998 for (const auto &info : supports) {
1999 auto sliceType = RankedTensorType::get({info.size}, elemType);
2000 auto slice = impulse::SliceOp::create(
2001 builder, loc, sliceType, constrained1D,
2002 builder.getDenseI64ArrayAttr({info.offset}),
2003 builder.getDenseI64ArrayAttr({info.offset + info.size}),
2004 builder.getDenseI64ArrayAttr({1}));
2005 Value unconstrainedSlice;
2006 if (info.support && info.support.getKind() != impulse::SupportKind::REAL) {
2007 unconstrainedSlice =
2008 transforms::unconstrain(builder, loc, slice, info.support);
2009 } else {
2010 unconstrainedSlice = slice;
2011 }
2012
2013 slices.push_back(unconstrainedSlice);
2014 }
2015
2016 Value result1D;
2017 if (slices.size() == 1) {
2018 result1D = slices[0];
2019 } else {
2020 auto resultType1D = RankedTensorType::get({positionSize}, elemType);
2021 result1D = arith::ConstantOp::create(
2022 builder, loc, resultType1D,
2023 DenseElementsAttr::get(resultType1D,
2024 builder.getFloatAttr(elemType, 0.0)));
2025
2026 auto i64ScalarType = RankedTensorType::get({}, builder.getI64Type());
2027 auto elemType1DSlice = RankedTensorType::get({1}, elemType);
2028 auto elemType0D = RankedTensorType::get({}, elemType);
2029 int64_t offset = 0;
2030 for (size_t i = 0; i < slices.size(); ++i) {
2031 auto sliceType = cast<RankedTensorType>(slices[i].getType());
2032 int64_t sliceSize = sliceType.getShape()[0];
2033
2034 for (int64_t j = 0; j < sliceSize; ++j) {
2035 auto elemIdx = arith::ConstantOp::create(
2036 builder, loc, i64ScalarType,
2037 DenseElementsAttr::get(i64ScalarType,
2038 builder.getI64IntegerAttr(j)));
2039 auto resultIdx = arith::ConstantOp::create(
2040 builder, loc, i64ScalarType,
2041 DenseElementsAttr::get(i64ScalarType,
2042 builder.getI64IntegerAttr(offset + j)));
2043
2044 auto elemSliced = impulse::DynamicSliceOp::create(
2045 builder, loc, elemType1DSlice, slices[i], ValueRange{elemIdx},
2046 builder.getDenseI64ArrayAttr({1}));
2047 auto elem =
2048 impulse::ReshapeOp::create(builder, loc, elemType0D, elemSliced);
2049
2050 auto resultSliced = impulse::ReshapeOp::create(
2051 builder, loc, RankedTensorType::get({1}, elemType), elem);
2052 result1D = impulse::DynamicUpdateSliceOp::create(
2053 builder, loc, resultType1D, result1D, resultSliced,
2054 ValueRange{resultIdx});
2055 }
2056
2057 offset += sliceSize;
2058 }
2059 }
2060
2061 auto resultType2D = RankedTensorType::get({1, positionSize}, elemType);
2062 return impulse::ReshapeOp::create(builder, loc, resultType2D, result1D);
2063}
2064
2065Value impulse::constrainPosition(OpBuilder &builder, Location loc,
2066 Value unconstrained,
2067 ArrayRef<SupportInfo> supports) {
2068 bool hasConstraints = false;
2069 for (const auto &info : supports) {
2070 if (info.support && info.support.getKind() != impulse::SupportKind::REAL) {
2071 hasConstraints = true;
2072 break;
2073 }
2074 }
2075 if (!hasConstraints || supports.empty())
2076 return unconstrained;
2077
2078 auto inputType = cast<RankedTensorType>(unconstrained.getType());
2079 auto elemType = inputType.getElementType();
2080 int64_t positionSize = inputType.getShape()[1];
2081
2082 auto positionType1D = RankedTensorType::get({positionSize}, elemType);
2083 Value unconstrained1D =
2084 impulse::ReshapeOp::create(builder, loc, positionType1D, unconstrained);
2085
2086 SmallVector<Value> slices;
2087 for (const auto &info : supports) {
2088 auto sliceType = RankedTensorType::get({info.size}, elemType);
2089 auto slice = impulse::SliceOp::create(
2090 builder, loc, sliceType, unconstrained1D,
2091 builder.getDenseI64ArrayAttr({info.offset}),
2092 builder.getDenseI64ArrayAttr({info.offset + info.size}),
2093 builder.getDenseI64ArrayAttr({1}));
2094
2095 Value constrainedSlice;
2096 if (info.support && info.support.getKind() != impulse::SupportKind::REAL) {
2097 constrainedSlice =
2098 transforms::constrain(builder, loc, slice, info.support);
2099 } else {
2100 constrainedSlice = slice;
2101 }
2102
2103 slices.push_back(constrainedSlice);
2104 }
2105
2106 Value result1D;
2107 if (slices.size() == 1) {
2108 result1D = slices[0];
2109 } else {
2110 auto resultType1D = RankedTensorType::get({positionSize}, elemType);
2111 result1D = arith::ConstantOp::create(
2112 builder, loc, resultType1D,
2113 DenseElementsAttr::get(resultType1D,
2114 builder.getFloatAttr(elemType, 0.0)));
2115
2116 auto i64ScalarType = RankedTensorType::get({}, builder.getI64Type());
2117 auto elemType1DSlice = RankedTensorType::get({1}, elemType);
2118 auto elemType0D = RankedTensorType::get({}, elemType);
2119 int64_t offset = 0;
2120 for (size_t i = 0; i < slices.size(); ++i) {
2121 auto sliceType = cast<RankedTensorType>(slices[i].getType());
2122 int64_t sliceSize = sliceType.getShape()[0];
2123
2124 for (int64_t j = 0; j < sliceSize; ++j) {
2125 auto elemIdx = arith::ConstantOp::create(
2126 builder, loc, i64ScalarType,
2127 DenseElementsAttr::get(i64ScalarType,
2128 builder.getI64IntegerAttr(j)));
2129 auto resultIdx = arith::ConstantOp::create(
2130 builder, loc, i64ScalarType,
2131 DenseElementsAttr::get(i64ScalarType,
2132 builder.getI64IntegerAttr(offset + j)));
2133
2134 auto elemSliced = impulse::DynamicSliceOp::create(
2135 builder, loc, elemType1DSlice, slices[i], ValueRange{elemIdx},
2136 builder.getDenseI64ArrayAttr({1}));
2137 auto elem =
2138 impulse::ReshapeOp::create(builder, loc, elemType0D, elemSliced);
2139
2140 auto resultSliced =
2141 impulse::ReshapeOp::create(builder, loc, elemType1DSlice, elem);
2142 result1D = impulse::DynamicUpdateSliceOp::create(
2143 builder, loc, resultType1D, result1D, resultSliced,
2144 ValueRange{resultIdx});
2145 }
2146
2147 offset += sliceSize;
2148 }
2149 }
2150
2151 auto resultType2D = RankedTensorType::get({1, positionSize}, elemType);
2152 return impulse::ReshapeOp::create(builder, loc, resultType2D, result1D);
2153}
2154
2155Value impulse::computeTotalJacobianCorrection(OpBuilder &builder, Location loc,
2156 Value unconstrained,
2157 ArrayRef<SupportInfo> supports) {
2158 auto inputType = cast<RankedTensorType>(unconstrained.getType());
2159 auto elemType = inputType.getElementType();
2160 auto scalarType = RankedTensorType::get({}, elemType);
2161
2162 Value total = arith::ConstantOp::create(
2163 builder, loc, scalarType,
2164 DenseElementsAttr::get(scalarType, builder.getFloatAttr(elemType, 0.0)));
2165
2166 if (supports.empty())
2167 return total;
2168
2169 int64_t positionSize = inputType.getShape()[1];
2170 auto positionType1D = RankedTensorType::get({positionSize}, elemType);
2171 Value unconstrained1D =
2172 impulse::ReshapeOp::create(builder, loc, positionType1D, unconstrained);
2173
2174 for (const auto &info : supports) {
2175 if (!info.support || info.support.getKind() == impulse::SupportKind::REAL)
2176 continue;
2177
2178 auto sliceType = RankedTensorType::get({info.size}, elemType);
2179 auto slice = impulse::SliceOp::create(
2180 builder, loc, sliceType, unconstrained1D,
2181 builder.getDenseI64ArrayAttr({info.offset}),
2182 builder.getDenseI64ArrayAttr({info.offset + info.size}),
2183 builder.getDenseI64ArrayAttr({1}));
2184 auto jacobian =
2185 transforms::logAbsDetJacobian(builder, loc, slice, info.support);
2186
2187 total = arith::AddFOp::create(builder, loc, total, jacobian);
2188 }
2189
2190 return total;
2191}
static Value gatherPositionFromTrace(OpBuilder &builder, Location loc, Value fullTrace, const HMCContext &ctx)
Definition HMCUtils.cpp:315
static Value createPermutationMatrix(OpBuilder &builder, Location loc, RankedTensorType matrixType)
Creates a permutation matrix of size n x n.
Definition HMCUtils.cpp:98
static Value scatterPositionToTrace(OpBuilder &builder, Location loc, Value position2d, Value fullTrace, const HMCContext &ctx)
Definition HMCUtils.cpp:281
static Value createIdentityMatrix(OpBuilder &builder, Location loc, RankedTensorType matrixType)
Creates a 2D identity matrix of the specified type.
Definition HMCUtils.cpp:75
static Value reverseRowsAndColumns(OpBuilder &builder, Location loc, Value matrix)
Computes A[::-1, ::-1] using permutation matrix through P @ A @ P.
Definition HMCUtils.cpp:121
PointerType * traceType(LLVMContext &C)
Value logAbsDetJacobian(OpBuilder &builder, Location loc, Value unconstrained, impulse::SupportAttr support)
Compute log |det J| of the transform from unconstrained to constrained.
Value unconstrain(OpBuilder &builder, Location loc, Value constrained, impulse::SupportAttr support)
Transform from constrained to unconstrained space.
Value constrain(OpBuilder &builder, Location loc, Value unconstrained, impulse::SupportAttr support)
Transform from unconstrained to constrained space.
Value computeTotalJacobianCorrection(OpBuilder &builder, Location loc, Value unconstrained, ArrayRef< SupportInfo > supports)
Compute total Jacobian correction for the constrain transform over all position vector slices.
Value finalizeWelford(OpBuilder &builder, Location loc, const WelfordState &state, const WelfordConfig &config)
Finalize Welford state to produce sample covariance (returned as inverse mass matrix).
Value checkTurning(OpBuilder &builder, Location loc, Value pLeft, Value pRight, Value pSum, const NUTSContext &ctx)
U-turn termination criterion.
Definition HMCUtils.cpp:509
GradientResult computePotentialAndGradient(OpBuilder &builder, Location loc, Value position, Value rng, const HMCContext &ctx)
Computes potential energy U(q) = -log p(q) and its gradient dU/dq
Definition HMCUtils.cpp:352
DualAveragingState initDualAveraging(OpBuilder &builder, Location loc, Value stepSize, bool logOfProduct=false)
Initialize dual averaging state from initial step size.
std::pair< Value, Value > leafIdxToCheckpointIdxs(OpBuilder &builder, Location loc, Value leafIdx)
Computes checkpoint indices from leaf index for iterative turning check.
IntegratorState getLeafFromTree(OpBuilder &builder, Location loc, const NUTSTreeState &tree, Value direction, const NUTSContext &ctx)
Extracts an appropriate leaf based on direction.
std::pair< Value, Value > updateCheckpoints(OpBuilder &builder, Location loc, Value leafIdx, Value ckptIdxMax, Value p, Value pSum, Value pCkpts, Value pSumCkpts, const NUTSContext &ctx, bool debugDump=false)
Update checkpoint arrays at even leaf indices.
SubtreeBuildResult buildIterativeSubtree(OpBuilder &builder, Location loc, const NUTSTreeState &initialTree, Value direction, Value pCkpts, Value pSumCkpts, const NUTSContext &ctx, bool debugDump=false)
Builds a subtree iteratively by appending leaves one at a time.
std::pair< Value, Value > sampleMomentum(OpBuilder &builder, Location loc, Value rng, Value invMass, Value massMatrixSqrt, RankedTensorType positionType, bool debugDump=false)
Samples momentum from N(0, M) where M is the mass matrix.
Definition HMCUtils.cpp:233
Value computeUniformTransitionProb(OpBuilder &builder, Location loc, Value currentWeight, Value newWeight)
Computes the uniform transition probability for subtree combination.
Definition HMCUtils.cpp:557
SmallVector< AdaptWindow > buildAdaptationSchedule(int64_t numSteps)
Build warmup adaptation schedule.
NUTSTreeState buildBaseTree(OpBuilder &builder, Location loc, const IntegratorState &leaf, Value rng, Value direction, const NUTSContext &ctx)
Builds a base tree (leaf node) by taking one leapfrog step.
Value computeKineticEnergy(OpBuilder &builder, Location loc, Value momentum, Value invMass, RankedTensorType positionType)
Computes K = 0.5 * p^T @ M^-1 @ p
Definition HMCUtils.cpp:166
NUTSTreeState combineTrees(OpBuilder &builder, Location loc, const NUTSTreeState &tree, const NUTSTreeState &subTree, Value direction, Value rng, bool biased, const NUTSContext &ctx)
Combines a tree with a newly-built subtree during NUTS doubling process.
Definition HMCUtils.cpp:590
Value computeBiasedTransitionProb(OpBuilder &builder, Location loc, Value currentWeight, Value newWeight, Value turning, Value diverging)
Computes the biased transition probability for main tree combination.
Definition HMCUtils.cpp:566
Value checkIterativeTurning(OpBuilder &builder, Location loc, Value p, Value pSum, Value pCkpts, Value pSumCkpts, Value idxMin, Value idxMax, const NUTSContext &ctx, bool debugDump=false)
Checkpoint-based iterative turning check.
NUTSTreeState buildTree(OpBuilder &builder, Location loc, const NUTSTreeState &initialTree, const NUTSContext &ctx, bool debugDump=false)
Main NUTS tree building loop.
Value conditionalDump(OpBuilder &builder, Location loc, Value value, StringRef label, bool debugDump)
Conditionally dump a value for debugging.
Definition HMCUtils.cpp:64
MCMCKernelResult SampleNUTS(OpBuilder &builder, Location loc, Value q, Value grad, Value U, Value rng, const NUTSContext &ctx, bool debugDump=false)
Single NUTS iteration: momentum sampling + tree building.
InitialHMCState InitHMC(OpBuilder &builder, Location loc, Value rng, const HMCContext &ctx, Value initialPosition=Value(), bool debugDump=false)
Initializes HMC/NUTS state from a trace Specifically:
Definition HMCUtils.cpp:701
SubtreeBuildResult doubleTree(OpBuilder &builder, Location loc, const NUTSTreeState &tree, Value direction, Value pCkpts, Value pSumCkpts, const NUTSContext &ctx, bool debugDump=false)
Tree doubling by building a subtree of same depth and combining.
IntegrationResult computeIntegrationStep(OpBuilder &builder, Location loc, const IntegratorState &leaf, Value rng, Value direction, const HMCContext &ctx)
Computes a single leapfrog integration step.
Definition HMCUtils.cpp:460
DualAveragingState updateDualAveraging(OpBuilder &builder, Location loc, const DualAveragingState &state, Value acceptProb, const DualAveragingConfig &config)
Update dual averaging state with observed acceptance probability.
Value unconstrainPosition(OpBuilder &builder, Location loc, Value constrained, ArrayRef< SupportInfo > supports)
Transform an entire position vector from constrained to unconstrained space based on the support info...
Value computeMassMatrixSqrt(OpBuilder &builder, Location loc, Value invMass, RankedTensorType positionType)
Computes the square root of the mass matrix from the inverse mass matrix.
Definition HMCUtils.cpp:190
WelfordState updateWelford(OpBuilder &builder, Location loc, const WelfordState &state, Value sample, const WelfordConfig &config)
Update Welford state with a new sample.
WelfordState initWelford(OpBuilder &builder, Location loc, int64_t positionSize, bool diagonal)
Initialize state for Welford covariance estimation.
Value getStepSizeFromDualAveraging(OpBuilder &builder, Location loc, const DualAveragingState &state, bool final=false)
Get step size from dual averaging state.
MCMCKernelResult SampleHMC(OpBuilder &builder, Location loc, Value q, Value grad, Value U, Value rng, const HMCContext &ctx, bool debugDump=false)
Single HMC iteration: momentum sampling + leapfrog + MH accept/reject.
Definition HMCUtils.cpp:862
Value constrainPosition(OpBuilder &builder, Location loc, Value unconstrained, ArrayRef< SupportInfo > supports)
Transform an entire position vector from unconstrained to constrained space.
Value applyInverseMassMatrix(OpBuilder &builder, Location loc, Value invMass, Value momentum, RankedTensorType positionType)
Computes v = M^-1 @ p If invMass is nullptr, returns momentum unchanged (assumes identity) If invMass...
Definition HMCUtils.cpp:139
DictionaryAttr autodiffAttrs
Definition HMCUtils.h:114
Type getElementType() const
Definition HMCUtils.h:147
FlatSymbolRefAttr fn
Definition HMCUtils.h:101
HMCContext withStepSize(Value newStepSize) const
Definition HMCUtils.h:167
ArrayRef< Value > fnInputs
Definition HMCUtils.h:102
RankedTensorType getPositionType() const
Definition HMCUtils.h:151
int64_t getFullTraceSize() const
Definition HMCUtils.h:142
SmallVector< Type > fnResultTypes
Definition HMCUtils.h:103
SmallVector< SupportInfo > supports
Definition HMCUtils.h:112
bool hasCustomLogpdf() const
Definition HMCUtils.h:140
RankedTensorType getScalarType() const
Definition HMCUtils.h:155
FlatSymbolRefAttr logpdfFn
Definition HMCUtils.h:113
Result of one MCMC kernel step.
Definition HMCUtils.h:62
NUTSContext withH0(Value newH0) const
Definition HMCUtils.h:200
SmallVector< Value > toValues() const
Definition HMCUtils.cpp:26
SmallVector< Type > getTypes() const
Definition HMCUtils.cpp:57
static NUTSTreeState fromValues(ArrayRef< Value > values)
Definition HMCUtils.cpp:35
Configuration for Welford covariance estimation.
Definition HMCUtils.h:422
State for Welford covariance estimation.
Definition HMCUtils.h:407