blob: 921dc20d1b8a12a25ae6a258c225df45b0ba15d7 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*!
* \file exec_context.cc
* \brief Compile-time active-thread state backed by TileLayout.
*/
#include <tvm/arith/analyzer.h>
#include <tvm/runtime/logging.h>
#include <tvm/tirx/exec_context.h>
#include <tvm/tirx/expr.h>
#include <algorithm>
#include <cstdlib>
#include <numeric>
#include <sstream>
#include <utility>
namespace tvm {
namespace tirx {
namespace {
constexpr int kWarpSize = 32;
PrimExpr I64(int64_t value) { return IntImm::Int64(value); }
AxisRange MakeRange(int64_t extent, int64_t offset = 0, int64_t stride = 1) {
return AxisRange{I64(extent), I64(offset), I64(stride)};
}
bool TryAsInt64(const PrimExpr& expr, int64_t* value) {
if (const auto* imm = expr.as<IntImmNode>()) {
*value = imm->value;
return true;
}
return false;
}
bool IsZero(const PrimExpr& expr) {
arith::Analyzer analyzer;
return analyzer->CanProveEqual(expr, 0);
}
ActiveSet MakeActiveSet(const std::vector<std::pair<std::string, AxisRange>>& axes) {
ffi::Array<Iter> shard;
ffi::Map<Axis, PrimExpr> offset;
for (const auto& [name, range] : axes) {
Axis axis = Axis::Get(name);
shard.push_back(Iter(range.extent, range.stride, axis));
if (!IsZero(range.offset)) {
offset.Set(axis, range.offset);
}
}
return ActiveSet{TileLayout(shard, {}, offset)};
}
std::vector<std::pair<std::string, AxisRange>> AxisRanges(const ActiveSet& A) {
std::vector<std::pair<std::string, AxisRange>> axes;
for (const auto& iter : A.layout->shard) {
AxisRange range;
TVM_FFI_ICHECK(A.GetAxis(iter->axis->name.operator std::string(), &range));
axes.push_back({iter->axis->name.operator std::string(), range});
}
return axes;
}
bool NarrowAxis(const ActiveSet& A, const std::string& axis, int64_t lo, int64_t hi, ActiveSet* out,
std::string* err) {
AxisRange cur;
if (!A.GetAxis(axis, &cur)) {
*err = "unknown active-set axis: " + axis;
return false;
}
AxisRange narrowed;
if (!cur.Intersect(lo, hi, &narrowed)) {
*err = "filter produces empty or non-structural active-set range on axis " + axis;
return false;
}
*out = A.WithAxis(axis, narrowed);
return true;
}
bool ModuloAxis(const ActiveSet& A, const std::string& axis, int64_t modulus, int64_t residue,
ActiveSet* out, std::string* err) {
AxisRange cur;
if (!A.GetAxis(axis, &cur)) {
*err = "unknown active-set axis: " + axis;
return false;
}
AxisRange narrowed;
if (!cur.Modulo(modulus, residue, &narrowed)) {
*err = "modulo filter produces empty or non-structural active-set slice on axis " + axis;
return false;
}
*out = A.WithAxis(axis, narrowed);
return true;
}
void AddCtaAxes(const ActiveSet& A, std::unordered_map<std::string, AxisRange>* side) {
AxisRange cta_id;
if (A.GetAxis("cta_id", &cta_id)) {
(*side)["cta_id"] = cta_id;
return;
}
for (const std::string& axis : A.AxisNames()) {
if (axis == "laneid" || axis == "warpid") continue;
AxisRange range;
TVM_FFI_ICHECK(A.GetAxis(axis, &range));
(*side)[axis] = range;
}
}
// Factor warpid into (wid_in_wg, wgid). Returns false on case 3 or symbolic offset.
bool FactorWarpid(const AxisRange& wp, AxisRange* wid_in_wg, AxisRange* wgid) {
int64_t off = 0;
int64_t ext = 0;
int64_t stride = 0;
if (!TryAsInt64(wp.offset, &off) || !TryAsInt64(wp.extent, &ext) ||
!TryAsInt64(wp.stride, &stride) || stride != 1) {
return false;
}
int64_t wid_off = off % kWgSize;
int64_t wgid_off = off / kWgSize;
if (wid_off == 0 && ext % kWgSize == 0) {
*wid_in_wg = MakeRange(kWgSize, 0);
*wgid = MakeRange(ext / kWgSize, wgid_off);
return true;
}
if (ext <= kWgSize - wid_off) {
*wid_in_wg = MakeRange(ext, wid_off);
*wgid = MakeRange(1, wgid_off);
return true;
}
return false;
}
int64_t FloorDivInt(int64_t a, int64_t b);
int64_t CeilDivInt(int64_t a, int64_t b);
bool SameIntRange(const AxisRange& lhs, const AxisRange& rhs) {
int64_t lhs_ext = 0;
int64_t lhs_off = 0;
int64_t lhs_stride = 0;
int64_t rhs_ext = 0;
int64_t rhs_off = 0;
int64_t rhs_stride = 0;
return TryAsInt64(lhs.extent, &lhs_ext) && TryAsInt64(lhs.offset, &lhs_off) &&
TryAsInt64(lhs.stride, &lhs_stride) && TryAsInt64(rhs.extent, &rhs_ext) &&
TryAsInt64(rhs.offset, &rhs_off) && TryAsInt64(rhs.stride, &rhs_stride) &&
lhs_ext == rhs_ext && lhs_off == rhs_off && lhs_stride == rhs_stride;
}
bool NarrowFlatProductRange(const AxisRange& major, const AxisRange& lane, int64_t lo, int64_t hi,
AxisRange* new_major, AxisRange* new_lane, std::string* err) {
int64_t major_off = 0;
int64_t major_ext = 0;
int64_t major_stride = 0;
int64_t lane_off = 0;
int64_t lane_ext = 0;
int64_t lane_stride = 0;
if (!TryAsInt64(major.offset, &major_off) || !TryAsInt64(major.extent, &major_ext) ||
!TryAsInt64(major.stride, &major_stride) || !TryAsInt64(lane.offset, &lane_off) ||
!TryAsInt64(lane.extent, &lane_ext) || !TryAsInt64(lane.stride, &lane_stride) ||
major_ext <= 0 || lane_ext <= 0 || major_stride <= 0 || lane_stride <= 0) {
*err = "flat thread range requires structural lane and warp axes";
return false;
}
int64_t active_min = major_off * kWarpSize + lane_off;
int64_t active_max = (major_off + major_stride * (major_ext - 1)) * kWarpSize +
(lane_off + lane_stride * (lane_ext - 1)) + 1;
if (lo <= active_min && active_max <= hi) {
*new_major = major;
*new_lane = lane;
return true;
}
if (major_stride != 1 || lane_stride != 1) {
*err = "flat thread range narrowing requires unit-stride lane and warp axes";
return false;
}
int64_t lane_hi = lane_off + lane_ext;
int64_t major_hi = major_off + major_ext;
int64_t hit_lo = std::max(major_off, FloorDivInt(lo - lane_hi, kWarpSize) + 1);
int64_t hit_hi = std::min(major_hi, CeilDivInt(hi - lane_off, kWarpSize));
if (hit_hi <= hit_lo) {
*err = "flat thread range produces empty active set";
return false;
}
if (hit_hi == hit_lo + 1) {
int64_t m = hit_lo;
int64_t new_lane_lo = std::max(lane_off, lo - m * kWarpSize);
int64_t new_lane_hi = std::min(lane_hi, hi - m * kWarpSize);
if (new_lane_hi <= new_lane_lo) {
*err = "flat thread range produces empty lane range";
return false;
}
*new_major = MakeRange(1, m);
*new_lane = MakeRange(new_lane_hi - new_lane_lo, new_lane_lo);
return true;
}
if (lo <= hit_lo * kWarpSize + lane_off && (hit_hi - 1) * kWarpSize + lane_hi <= hi) {
*new_major = MakeRange(hit_hi - hit_lo, hit_lo);
*new_lane = lane;
return true;
}
*err = "flat thread range would require a non-rectangular lane/warp active set";
return false;
}
bool NarrowFlatCtaThreadRange(const ActiveSet& A, int64_t lo, int64_t hi, ActiveSet* out,
std::string* err) {
AxisRange lane;
AxisRange warpid;
if (!A.GetAxis("laneid", &lane) || !A.GetAxis("warpid", &warpid)) {
*err = "active set has no laneid/warpid axes";
return false;
}
AxisRange new_lane;
AxisRange new_warpid;
if (!NarrowFlatProductRange(warpid, lane, lo, hi, &new_warpid, &new_lane, err)) {
return false;
}
*out = A.WithAxis("laneid", new_lane).WithAxis("warpid", new_warpid);
return true;
}
bool NarrowFlatWarpgroupThreadRange(const ActiveSet& A, int64_t lo, int64_t hi, ActiveSet* out,
std::string* err) {
AxisRange lane;
AxisRange warpid;
if (!A.GetAxis("laneid", &lane) || !A.GetAxis("warpid", &warpid)) {
*err = "active set has no laneid/warpid axes";
return false;
}
AxisRange wid_in_wg;
AxisRange wgid;
if (!FactorWarpid(warpid, &wid_in_wg, &wgid)) {
*err = "filter on flat warpgroup-thread range requires factorable warpid axis";
return false;
}
AxisRange new_lane;
AxisRange new_wid_in_wg;
if (!NarrowFlatProductRange(wid_in_wg, lane, lo, hi, &new_wid_in_wg, &new_lane, err)) {
return false;
}
int64_t wgid_ext = 0;
int64_t wgid_off = 0;
if (!TryAsInt64(wgid.extent, &wgid_ext) || !TryAsInt64(wgid.offset, &wgid_off)) {
*err = "filter on flat warpgroup-thread range requires structural warpgroup id";
return false;
}
if (wgid_ext != 1) {
if (SameIntRange(new_lane, lane) && SameIntRange(new_wid_in_wg, wid_in_wg)) {
*out = A;
return true;
}
*err = "flat warpgroup-thread range across multiple warpgroups is not representable";
return false;
}
int64_t wid_ext = 0;
int64_t wid_off = 0;
if (!TryAsInt64(new_wid_in_wg.extent, &wid_ext) || !TryAsInt64(new_wid_in_wg.offset, &wid_off)) {
*err = "filter on flat warpgroup-thread range requires structural warp id";
return false;
}
*out = A.WithAxis("laneid", new_lane)
.WithAxis("warpid", MakeRange(wid_ext, wgid_off * kWgSize + wid_off));
return true;
}
int64_t FloorDivInt(int64_t a, int64_t b) {
TVM_FFI_ICHECK_GT(b, 0);
if (a >= 0) return a / b;
return -static_cast<int64_t>((static_cast<uint64_t>(-a) + b - 1) / b);
}
int64_t CeilDivInt(int64_t a, int64_t b) { return -FloorDivInt(-a, b); }
int64_t NormalizeMod(int64_t value, int64_t modulus) {
int64_t ret = value % modulus;
if (ret < 0) ret += modulus;
return ret;
}
int64_t ExtendedGcd(int64_t a, int64_t b, int64_t* x, int64_t* y) {
if (b == 0) {
*x = 1;
*y = 0;
return a;
}
int64_t x1 = 0;
int64_t y1 = 0;
int64_t g = ExtendedGcd(b, a % b, &x1, &y1);
*x = y1;
*y = x1 - (a / b) * y1;
return g;
}
int64_t ModularInverse(int64_t value, int64_t modulus) {
int64_t x = 0;
int64_t y = 0;
int64_t g = ExtendedGcd(NormalizeMod(value, modulus), modulus, &x, &y);
TVM_FFI_ICHECK_EQ(g, 1);
return NormalizeMod(x, modulus);
}
} // namespace
bool AxisRange::Intersect(int64_t lo, int64_t hi, AxisRange* out) const {
int64_t cur_off = 0;
int64_t cur_ext = 0;
int64_t cur_stride = 0;
if (!TryAsInt64(offset, &cur_off) || !TryAsInt64(extent, &cur_ext) ||
!TryAsInt64(stride, &cur_stride) || cur_stride <= 0) {
return false;
}
int64_t i_lo = std::max<int64_t>(0, CeilDivInt(lo - cur_off, cur_stride));
int64_t i_hi = std::min<int64_t>(cur_ext, FloorDivInt(hi - 1 - cur_off, cur_stride) + 1);
if (i_hi <= i_lo) return false;
out->extent = I64(i_hi - i_lo);
out->offset = I64(cur_off + cur_stride * i_lo);
out->stride = I64(cur_stride);
return true;
}
bool AxisRange::Modulo(int64_t modulus, int64_t residue, AxisRange* out) const {
if (modulus <= 0) return false;
int64_t cur_off = 0;
int64_t cur_ext = 0;
int64_t cur_stride = 0;
if (!TryAsInt64(offset, &cur_off) || !TryAsInt64(extent, &cur_ext) ||
!TryAsInt64(stride, &cur_stride) || cur_stride <= 0) {
return false;
}
residue = NormalizeMod(residue, modulus);
int64_t rhs = NormalizeMod(residue - cur_off, modulus);
int64_t g = std::gcd(std::llabs(cur_stride), std::llabs(modulus));
if (rhs % g != 0) return false;
int64_t reduced_stride = cur_stride / g;
int64_t reduced_rhs = rhs / g;
int64_t reduced_modulus = modulus / g;
int64_t period = reduced_modulus;
int64_t i0 =
NormalizeMod(reduced_rhs * ModularInverse(reduced_stride, reduced_modulus), reduced_modulus);
if (i0 >= cur_ext) return false;
int64_t new_ext = (cur_ext - 1 - i0) / period + 1;
out->extent = I64(new_ext);
out->offset = I64(cur_off + cur_stride * i0);
out->stride = I64(cur_stride * period);
return true;
}
bool ActiveSet::GetAxis(const std::string& axis, AxisRange* out) const {
if (!layout.defined()) return false;
for (const auto& iter : layout->shard) {
if (iter->axis->name != axis) continue;
PrimExpr off = I64(0);
for (const auto& kv : layout->offset) {
if (kv.first->name == axis) {
off = kv.second;
break;
}
}
*out = AxisRange{iter->extent, off, iter->stride};
return true;
}
return false;
}
bool ActiveSet::HasAxis(const std::string& axis) const {
AxisRange ignored;
return GetAxis(axis, &ignored);
}
ActiveSet ActiveSet::WithAxis(const std::string& axis, const AxisRange& range) const {
std::vector<std::pair<std::string, AxisRange>> axes = AxisRanges(*this);
bool found = false;
for (auto& entry : axes) {
if (entry.first == axis) {
entry.second = range;
found = true;
break;
}
}
TVM_FFI_ICHECK(found) << "Internal Error: unknown active-set axis " << axis;
return MakeActiveSet(axes);
}
std::vector<std::string> ActiveSet::AxisNames() const {
std::vector<std::string> names;
if (!layout.defined()) return names;
for (const auto& iter : layout->shard) {
names.push_back(iter->axis->name.operator std::string());
}
return names;
}
int64_t ActiveSet::size() const {
int64_t size = 1;
for (const auto& iter : layout->shard) {
int64_t extent = 0;
if (!TryAsInt64(iter->extent, &extent)) return 0;
size *= extent;
}
return size;
}
ActiveSet InitialActiveSet(int64_t lane_ext, int64_t warp_ext, int64_t cta_ext) {
return InitialActiveSet(lane_ext, warp_ext, cta_ext, {});
}
ActiveSet InitialActiveSet(int64_t lane_ext, int64_t warp_ext, int64_t cta_ext,
const std::vector<std::pair<std::string, int64_t>>& cta_axes) {
std::vector<std::pair<std::string, AxisRange>> axes = {{"laneid", MakeRange(lane_ext)},
{"warpid", MakeRange(warp_ext)}};
if (cta_axes.empty()) {
axes.push_back({"cta_id", MakeRange(cta_ext)});
} else {
for (const auto& [axis, extent] : cta_axes) {
axes.push_back({axis, MakeRange(extent)});
}
}
return MakeActiveSet(axes);
}
bool FilterNarrow(const ActiveSet& A, ScopeBinding binding, int64_t lo, int64_t hi, ActiveSet* out,
std::string* err) {
if (lo >= hi) {
*err = "filter range is empty or inverted";
return false;
}
switch (binding) {
case ScopeBinding::kWarpThread:
return NarrowAxis(A, "laneid", lo, hi, out, err);
case ScopeBinding::kCtaWarp:
return NarrowAxis(A, "warpid", lo, hi, out, err);
case ScopeBinding::kKernelCta:
case ScopeBinding::kClusterCta:
return NarrowAxis(A, "cta_id", lo, hi, out, err);
case ScopeBinding::kCtaWarpgroup: {
AxisRange wp;
if (!A.GetAxis("warpid", &wp)) {
*err = "active set has no warpid axis";
return false;
}
int64_t wp_off = 0;
int64_t wp_ext = 0;
if (!TryAsInt64(wp.offset, &wp_off) || !TryAsInt64(wp.extent, &wp_ext)) {
*err = "filter on warpgroup_id requires structural warpid offset";
return false;
}
if (wp_off % kWgSize != 0 || wp_ext % kWgSize != 0) {
*err = "filter on warpgroup_id requires warpid axis aligned to WG_SIZE";
return false;
}
AxisRange cur_outer = MakeRange(wp_ext / kWgSize, wp_off / kWgSize);
AxisRange new_outer;
if (!cur_outer.Intersect(lo, hi, &new_outer)) {
*err = "filter on warpgroup_id produces empty range";
return false;
}
int64_t outer_ext = 0;
int64_t outer_off = 0;
TVM_FFI_ICHECK(TryAsInt64(new_outer.extent, &outer_ext));
TVM_FFI_ICHECK(TryAsInt64(new_outer.offset, &outer_off));
*out = A.WithAxis("warpid", MakeRange(outer_ext * kWgSize, outer_off * kWgSize));
return true;
}
case ScopeBinding::kWarpgroupWarp: {
AxisRange wp;
if (!A.GetAxis("warpid", &wp)) {
*err = "active set has no warpid axis";
return false;
}
int64_t wp_off = 0;
int64_t wp_ext = 0;
if (!TryAsInt64(wp.offset, &wp_off) || !TryAsInt64(wp.extent, &wp_ext)) {
*err = "filter on warp_id_in_wg requires structural warpid offset";
return false;
}
int64_t cur_inner_off = wp_off % kWgSize;
if (wp_ext > kWgSize - cur_inner_off) {
*err = "filter on warp_id_in_wg would break active-set TileLayout box";
return false;
}
AxisRange cur_inner = MakeRange(wp_ext, cur_inner_off);
AxisRange new_inner;
if (!cur_inner.Intersect(lo, hi, &new_inner)) {
*err = "filter on warp_id_in_wg produces empty range";
return false;
}
int64_t inner_ext = 0;
int64_t inner_off = 0;
TVM_FFI_ICHECK(TryAsInt64(new_inner.extent, &inner_ext));
TVM_FFI_ICHECK(TryAsInt64(new_inner.offset, &inner_off));
int64_t outer_base = (wp_off / kWgSize) * kWgSize;
*out = A.WithAxis("warpid", MakeRange(inner_ext, outer_base + inner_off));
return true;
}
case ScopeBinding::kKernelCluster:
*err = "filter on cluster_id is not supported";
return false;
case ScopeBinding::kClusterCtaPair:
*err = "filter on cta_id_in_pair must be lowered through CTA pair modulo analysis";
return false;
case ScopeBinding::kCtaThread:
return NarrowFlatCtaThreadRange(A, lo, hi, out, err);
case ScopeBinding::kWarpgroupThread:
return NarrowFlatWarpgroupThreadRange(A, lo, hi, out, err);
}
*err = "unknown ScopeBinding";
return false;
}
bool ScopeSwitch(const ActiveSet& A, ScopeKind scope_kind, ExecSplit* out, std::string* err) {
out->inter.clear();
out->intra.clear();
AxisRange laneid;
AxisRange warpid;
TVM_FFI_ICHECK(A.GetAxis("laneid", &laneid));
TVM_FFI_ICHECK(A.GetAxis("warpid", &warpid));
switch (scope_kind) {
case ScopeKind::kThread:
out->inter["laneid"] = laneid;
out->inter["warpid"] = warpid;
AddCtaAxes(A, &out->inter);
return true;
case ScopeKind::kWarp:
out->intra["laneid"] = laneid;
out->inter["warpid"] = warpid;
AddCtaAxes(A, &out->inter);
return true;
case ScopeKind::kCta:
out->intra["laneid"] = laneid;
out->intra["warpid"] = warpid;
AddCtaAxes(A, &out->inter);
return true;
case ScopeKind::kCluster:
out->intra["laneid"] = laneid;
out->intra["warpid"] = warpid;
AddCtaAxes(A, &out->intra);
return true;
case ScopeKind::kWarpgroup: {
AxisRange wid_in_wg;
AxisRange wgid;
if (!FactorWarpid(warpid, &wid_in_wg, &wgid)) {
std::ostringstream os;
os << "scope_switch(warpgroup) failed: warpid TileLayout axis crosses warpgroup boundary "
"or has symbolic offset";
*err = os.str();
return false;
}
out->intra["laneid"] = laneid;
out->intra["wid_in_wg"] = wid_in_wg;
out->inter["wgid"] = wgid;
AddCtaAxes(A, &out->inter);
return true;
}
}
*err = "unknown ScopeKind";
return false;
}
ExecContext ExecContext::AtKernelEntry(int64_t lane_ext, int64_t warp_ext, int64_t cta_ext) {
return AtKernelEntry(lane_ext, warp_ext, cta_ext, {});
}
ExecContext ExecContext::AtKernelEntry(
int64_t lane_ext, int64_t warp_ext, int64_t cta_ext,
const std::vector<std::pair<std::string, int64_t>>& cta_axes) {
ExecContext ctx;
ctx.A = InitialActiveSet(lane_ext, warp_ext, cta_ext, cta_axes);
ctx.scope_kind = ScopeKind::kThread;
std::string err;
bool ok = ScopeSwitch(ctx.A, ctx.scope_kind, &ctx.split, &err);
(void)ok;
return ctx;
}
bool ExecContext::WithFilter(ScopeBinding binding, int64_t lo, int64_t hi, ExecContext* out,
std::string* err) const {
ActiveSet new_A;
if (!FilterNarrow(A, binding, lo, hi, &new_A, err)) return false;
ExecSplit new_split;
if (!ScopeSwitch(new_A, scope_kind, &new_split, err)) return false;
out->A = new_A;
out->scope_kind = scope_kind;
out->split = std::move(new_split);
return true;
}
bool ExecContext::WithSelector(ScopeBinding binding, PrimExpr selector, ExecContext* out,
std::string* err) const {
if (binding != ScopeBinding::kWarpThread) {
*err = "selector filter currently requires a lane_id / warp->thread binding";
return false;
}
ActiveSet new_A = A.WithAxis("laneid", AxisRange{I64(1), selector, I64(1)});
ExecSplit new_split;
if (!ScopeSwitch(new_A, scope_kind, &new_split, err)) return false;
out->A = std::move(new_A);
out->scope_kind = scope_kind;
out->split = std::move(new_split);
return true;
}
bool ExecContext::WithCtaAxisFilter(const std::string& axis, int64_t lo, int64_t hi,
ExecContext* out, std::string* err) const {
if (lo >= hi) {
*err = "filter range is empty or inverted";
return false;
}
ActiveSet new_A;
if (!NarrowAxis(A, axis, lo, hi, &new_A, err)) return false;
ExecSplit new_split;
if (!ScopeSwitch(new_A, scope_kind, &new_split, err)) return false;
out->A = std::move(new_A);
out->scope_kind = scope_kind;
out->split = std::move(new_split);
return true;
}
bool ExecContext::WithCtaAxisModulo(const std::string& axis, int64_t modulus, int64_t residue,
ExecContext* out, std::string* err) const {
ActiveSet new_A;
if (!ModuloAxis(A, axis, modulus, residue, &new_A, err)) return false;
ExecSplit new_split;
if (!ScopeSwitch(new_A, scope_kind, &new_split, err)) return false;
out->A = std::move(new_A);
out->scope_kind = scope_kind;
out->split = std::move(new_split);
return true;
}
ffi::Map<ffi::String, ffi::Array<PrimExpr>> EncodeSplitSide(
const std::unordered_map<std::string, AxisRange>& side) {
ffi::Map<ffi::String, ffi::Array<PrimExpr>> out;
for (const auto& kv : side) {
if (IsZero(kv.second.stride - I64(1))) {
out.Set(ffi::String(kv.first), ffi::Array<PrimExpr>{kv.second.extent, kv.second.offset});
} else {
out.Set(ffi::String(kv.first),
ffi::Array<PrimExpr>{kv.second.extent, kv.second.offset, kv.second.stride});
}
}
return out;
}
} // namespace tirx
} // namespace tvm