Skip to content

Conversation

@kurisu6912
Copy link
Collaborator

@kurisu6912 kurisu6912 commented Nov 5, 2025

This pr add a LegalizeNegativeIndex pass to handle negative index in BufferLoad operation.

It tries to handle each dimension:

  1. proved positive, do nothing
  2. proved negative, convert it to shape[i] + idx[i]
  3. otherwise, generating a warning to user

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced automatic legalization of negative indices in buffer operations, converting them to equivalent positive index expressions within the compilation pipeline.
  • Tests

    • Added comprehensive test coverage for negative index legalization, including scalar, affine expression, and symbolic index scenarios.

@github-actions
Copy link

github-actions bot commented Nov 5, 2025

👋 Hi! Thank you for contributing to the TileLang project.

Please remember to run pre-commit run --all-files in the root directory of the project to ensure your changes are properly linted and formatted. This will help ensure your contribution passes the format check.

We appreciate you taking this step! Our team will review your contribution, and we look forward to your awesome work! 🚀

@kurisu6912 kurisu6912 linked an issue Nov 5, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 5, 2025

Walkthrough

A new transform pass is introduced to legalize negative indices in buffer operations. The implementation includes an analyzer to detect negative indices, a rewriter to convert them to non-negative equivalents, and integration into the transformation pipeline with corresponding test coverage.

Changes

Cohort / File(s) Summary
Core Negative Index Legalization
src/transform/legalize_negative_index.cc
Implements NegativeIndexAnalyzer and NegativeIndexRewriter classes to scan and transform negative buffer indices. Exposes LegalizeNegativeIndex(PrimFunc) function and registers tl.LegalizeNegativeIndex pass via FFI.
Pipeline Integration
tilelang/engine/phase.py, tilelang/transform/__init__.py
Adds LegalizeNegativeIndex transformation step after single-buffer wrapper in LowerAndLegalize phase and exposes public API wrapper with documentation.
Test Coverage
testing/python/language/test_tilelang_language_negative_index.py
Defines before/after test functions for scalar, affine, and symbolic negative index cases with three corresponding test cases validating transformation correctness.

Sequence Diagram

sequenceDiagram
    participant PrimFunc as PrimFunc
    participant Analyzer as NegativeIndexAnalyzer
    participant Rewriter as NegativeIndexRewriter
    participant Result as Transformed Function

    PrimFunc->>Analyzer: Scan function body
    Analyzer->>Analyzer: Classify each BufferLoad index<br/>(NonNegative/Negative/Unknown)
    Analyzer-->>PrimFunc: Return state vectors per load
    
    rect rgba(100, 150, 200, 0.2)
    note over PrimFunc: If negative indices detected
    PrimFunc->>Rewriter: Pass state vectors
    Rewriter->>Rewriter: Rewrite BufferLoad nodes<br/>with index = extent + index
    Rewriter-->>Result: Return new PrimFunc
    end
    
    rect rgba(150, 200, 100, 0.2)
    note over PrimFunc: If no changes needed
    PrimFunc-->>Result: Return original function
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • src/transform/legalize_negative_index.cc — Dense implementation with dual analyzer/rewriter pattern; requires careful verification of index transformation logic and state management across BufferLoad nodes
  • Integration points — Review placement within LowerAndLegalize pipeline and interaction with InjectAssumes to ensure correct ordering and pass behavior
  • Test coverage — Validate that scalar, affine, and symbolic test cases accurately reflect expected transformations and cover edge cases

Poem

🐰 Behold! A transform most merry,
Where negative indices turn contrary—
No longer trapped in the minus abyss,
They dance with extents in algebraic bliss!
Legalize, transform, let the indices flow,
From shadow to light with a mathematical glow! ✨

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change—a new pass to handle negative indices—and is directly supported by the implementation adding LegalizeNegativeIndex pass.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
testing/python/language/test_tilelang_language_negative_index.py (1)

41-56: Add coverage for negative-index stores

Once the pass handles stores, please add a regression that writes through a negative index (e.g. B[T.int32(-1)] = ...) and asserts the rewritten store uses extent + index. It will prevent future regressions around the BufferStore branch.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 055f850 and b43a7c0.

📒 Files selected for processing (4)
  • src/transform/legalize_negative_index.cc (1 hunks)
  • testing/python/language/test_tilelang_language_negative_index.py (1 hunks)
  • tilelang/engine/phase.py (1 hunks)
  • tilelang/transform/__init__.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
tilelang/transform/__init__.py (1)
src/transform/legalize_negative_index.cc (2)
  • LegalizeNegativeIndex (129-143)
  • LegalizeNegativeIndex (129-129)
testing/python/language/test_tilelang_language_negative_index.py (3)
tilelang/language/ast/ir.py (1)
  • func_attr (217-225)
src/transform/legalize_negative_index.cc (2)
  • LegalizeNegativeIndex (129-143)
  • LegalizeNegativeIndex (129-129)
tilelang/transform/__init__.py (1)
  • LegalizeNegativeIndex (83-91)
tilelang/engine/phase.py (2)
src/transform/legalize_negative_index.cc (2)
  • LegalizeNegativeIndex (129-143)
  • LegalizeNegativeIndex (129-129)
tilelang/transform/__init__.py (1)
  • LegalizeNegativeIndex (83-91)
src/transform/legalize_negative_index.cc (1)
tilelang/transform/__init__.py (1)
  • LegalizeNegativeIndex (83-91)

Comment on lines +25 to +123
class NegativeIndexAnalyzer : public IRVisitorWithAnalyzer {
public:
explicit NegativeIndexAnalyzer(
std::unordered_map<const BufferLoadNode *, std::vector<IndexSignState>>
*result)
: result_(result) {}

void VisitExpr_(const BufferLoadNode *op) final {
auto load = tvm::ffi::GetRef<BufferLoad>(op);
std::vector<IndexSignState> states;
states.reserve(op->indices.size());
bool needs_record = false;

for (size_t i = 0; i < op->indices.size(); ++i) {
PrimExpr simplified = analyzer_.Simplify(op->indices[i]);
if (analyzer_.CanProve(simplified >= 0)) {
states.push_back(IndexSignState::kNonNegative);
continue;
}

if (analyzer_.CanProve(simplified < 0)) {
states.push_back(IndexSignState::kNegative);
needs_record = true;
continue;
}

states.push_back(IndexSignState::kUnknown);
needs_record = true;
LOG(WARNING) << "LegalizeNegativeIndex: cannot prove non-negative index "
<< simplified << " for buffer " << load->buffer->name
<< " (axis " << i << ").";
}

if (needs_record) {
(*result_)[op] = std::move(states);
}

IRVisitorWithAnalyzer::VisitExpr_(op);
}

private:
std::unordered_map<const BufferLoadNode *, std::vector<IndexSignState>>
*result_;
};

class NegativeIndexRewriter : public arith::IRMutatorWithAnalyzer {
public:
static PrimFunc
Apply(PrimFunc func,
const std::unordered_map<const BufferLoadNode *,
std::vector<IndexSignState>> &states) {
arith::Analyzer analyzer;
NegativeIndexRewriter rewriter(&analyzer, states);
if (!func->body.defined()) {
return func;
}
PrimFuncNode *func_node = func.CopyOnWrite();
func_node->body = rewriter.VisitStmt(func_node->body);
return func;
}

private:
NegativeIndexRewriter(
arith::Analyzer *analyzer,
const std::unordered_map<const BufferLoadNode *,
std::vector<IndexSignState>> &states)
: arith::IRMutatorWithAnalyzer(analyzer), states_(states) {}

PrimExpr VisitExpr_(const BufferLoadNode *op) final {
BufferLoad load =
Downcast<BufferLoad>(arith::IRMutatorWithAnalyzer::VisitExpr_(op));

auto it = states_.find(op);
if (it == states_.end()) {
return load;
}

auto indices = load->indices;
bool changed = false;

const auto &state_vector = it->second;
ICHECK_EQ(state_vector.size(), indices.size())
<< "State vector size mismatch for buffer load " << load->buffer->name;

for (size_t i = 0; i < indices.size(); ++i) {
if (state_vector[i] != IndexSignState::kNegative) {
continue;
}
PrimExpr extent = load->buffer->shape[i];
indices.Set(i, analyzer_->Simplify(extent + indices[i]));
changed = true;
}

if (!changed) {
return load;
}

return BufferLoad(load->buffer, indices);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Negative index BufferStore remains unhandled

The analyzer/rewriter only records BufferLoadNode indices, so a PrimFunc that writes through a negative index (e.g. A[T.int32(-1)] = value) survives this pass unchanged. Because LowerAndLegalize now runs this pass unconditionally, we still exit with negative indices on the store side, defeating the stated goal of canonicalizing indices before downstream passes.

Please extend the pass to cover BufferStoreNode as well (analyze its indices and rewrite them in VisitStmt_). A minimal shape for the fix:

-  void VisitExpr_(const BufferLoadNode* op) final {
-    ...
-      (*result_)[op] = std::move(states);
-    }
-    IRVisitorWithAnalyzer::VisitExpr_(op);
-  }
+  template <typename TNode>
+  void RecordIndices(const TNode* op, const Array<PrimExpr>& indices, const Buffer& buffer) {
+    ...
+      (*result_)[op] = std::move(states);
+    }
+  }
+
+  void VisitExpr_(const BufferLoadNode* op) final {
+    RecordIndices(op, op->indices, op->buffer);
+    IRVisitorWithAnalyzer::VisitExpr_(op);
+  }
+
+  void VisitStmt_(const BufferStoreNode* op) final {
+    RecordIndices(op, op->indices, op->buffer);
+    IRVisitorWithAnalyzer::VisitStmt_(op);
+  }

and mirror the same lookup/rewrite logic in NegativeIndexRewriter::VisitStmt_(const BufferStoreNode*).

This keeps the pipeline guarantee intact and prevents negative store indices from leaking past legalization.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/transform/legalize_negative_index.cc around lines 25-123, the pass only
analyzes/re-writes BufferLoadNode indices so negative indices in BufferStoreNode
survive; extend the pass to handle stores: add an analyzer VisitStmt_(const
BufferStoreNode*) that mirrors the BufferLoad logic (simplify each index,
classify as kNonNegative/kNegative/kUnknown, set needs_record and log warnings,
and record the per-index IndexSignState into a store-specific map), and add a
corresponding NegativeIndexRewriter::VisitStmt_(const BufferStoreNode*) that
looks up the recorded state vector for that store, asserts sizes match, rewrites
negative indices to extent + index (using analyzer_->Simplify) and returns a
modified BufferStore stmt when changed; update Apply and the pass state to
carry/store both load and store maps (or add a separate store map) so both
analysis and rewriting use the store information.

@LeiWang1999
Copy link
Member

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@LeiWang1999 LeiWang1999 merged commit 0592834 into tile-ai:main Nov 6, 2025
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature request] Support negative range as indices

2 participants