agentleFS
LuisaGroupLuisaGroup/LuisaCompute/.agents/skills/spv-opt/SKILL.md

spv-opt

Skill1.1k starsChanged 35 days agoApache-2.0View on GitHub

SPIRV-Tools optimizer pass development, IR manipulation, PassTest registration, and LuisaCompute spvtools::Optimizer integration (presets, LUISA_SPIRV_OPT_* env knobs).

Trending

Quiet since July.thin dataabout the whole repository

Dec 2025: 0 mentionsJan 2026: 0 mentionsFeb 2026: 0 mentionsMar 2026: 0 mentionsApr 2026: 0 mentionsMay 2026: 0 mentionsJun 2026: 0 mentionsJul 2026: 1 mentionAug 2026: 0 mentionsSep 2026: 0 mentionsOct 2026: 0 mentionsNov 2026: 0 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the LuisaGroup/LuisaCompute repository that our weekly crawl found: only 5 in all, so this is a hint, not a trend. There is no adoption data for this file yet.

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 1 is distinctive, and 11 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If you modify the module, return Status::SuccessWithChange.

    line 55
  2. Distinctive:

    When deleting instructions while iterating, collect first and kill after:

    line 224
  3. Very specific:

    Add header to source/opt/passes.h (or include directly).

    line 332
  4. Very specific:

    Add CreateMyPassPass() factory declaration to include/spirv-tools/optimizer.hpp:

    line 333
  5. Very specific:

    Add CLI flag mapping in source/opt/optimizer.cpp inside Optimizer::RegisterPassFromFlag:

    line 345
  6. Very specific:

    Add source files to source/opt/CMakeLists.txt:

    line 352
  7. Very specific:

    Add a test target/file under test/opt/ (e.g.

    line 360
  8. Very specific:

    Look at nearby passes in RegisterPassFromFlag for the exact pattern.

    line 362
  9. Very specific:

    After Process() returns SuccessWithChange, Pass::Run() itself (not PassManager) automatically calls:

    line 375
  10. Very specific:

    If you mutate IDs outside normal helpers (e.g.

    line 380
  11. Very specific:

    If you need an analysis inside Process() and are not preserving it, it is usually fine to request it via ctx->get_def_use_mgr() etc..

    line 382
  12. Very specific:

    All three also have overloads taking a bool preserve_interface argument.

    line 398

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

SPIRV-Tools Optimizer

Writing optimizer passes for SPIRV-Tools (src/ext/SPIRV-Tools).

Pass Skeleton

Derive from spvtools::opt::Pass (or MemPass for mem2reg-style passes), implement name() and Process().

// source/opt/my_pass.h
#ifndef SOURCE_OPT_MY_PASS_H_
#define SOURCE_OPT_MY_PASS_H_
#include "source/opt/pass.h"
namespace spvtools::opt {
class MyPass : public Pass {
 public:
  const char* name() const override { return "my-pass"; }
  Status Process() override;
  IRContext::Analysis GetPreservedAnalyses() override {
    return IRContext::kAnalysisDefUse | IRContext::kAnalysisCFG;
  }
};
}  // namespace spvtools::opt
#endif
// source/opt/my_pass.cpp
#include "source/opt/my_pass.h"
namespace spvtools::opt {
Pass::Status MyPass::Process() {
  bool modified = false;
  // Iterate functions
  for (auto& func : *get_module()) {
    for (auto& block : func) {
      for (auto& inst : block) {
        // transform inst...
        modified = true;
      }
    }
  }
  return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
}
}  // namespace spvtools::opt

Rules:

  • name() must match the --my-pass CLI flag used in RegisterPassFromFlag (no leading hyphens).
  • Process() must return Status::Failure only on real errors.
  • If you modify the module, return Status::SuccessWithChange; Pass::Run() invalidates analyses not listed in GetPreservedAnalyses() (source/opt/pass.cpp).
  • A single pass instance may only run once; internal state does not reset.

MemPass base class

Many load/store elimination passes derive from MemPass instead of Pass:

#include "source/opt/mem_pass.h"
class MyMemPass : public MemPass {
  // Inherits helpers: GetPtr(), IsTargetVar(), CollectTargetVars(),
  // HasOnlyNamesAndDecorates(), KillAllInsts(), Type2Undef(), etc.
};

Key APIs

Module / IRContext

Module* m = get_module();           // or context()->module()
IRContext* ctx = context();

m->ForEachInst([](Instruction* inst){ /* all insts */ }, true);
ctx->get_def_use_mgr();             // analysis::DefUseManager
ctx->get_type_mgr();                // analysis::TypeManager
ctx->get_constant_mgr();            // analysis::ConstantManager
ctx->get_decoration_mgr();          // analysis::DecorationManager
ctx->cfg();                         // CFG
ctx->GetValueNumberTable();         // ValueNumberTable
ctx->GetStructuredCFGAnalysis();    // StructuredCFGAnalysis
ctx->InvalidateAnalyses(IRContext::kAnalysisDefUse | IRContext::kAnalysisCFG);
ctx->IsConsistent();                // debug invariant check

Instruction

spv::Op opcode = inst->opcode();
uint32_t rid = inst->result_id();   // 0 if none
uint32_t tid = inst->type_id();     // 0 if none

// Operands: inst->begin() .. inst->end()
for (auto& op : *inst) {
  if (spvIsIdType(op.type)) { uint32_t id = op.words[0]; }
}

uint32_t val = inst->GetSingleWordInOperand(idx);
inst->NumInOperands();
inst->SetResultId(new_id);
inst->SetResultType(new_type_id);
inst->SetInOperand(idx, {new_val});
inst->ToBinaryWithoutAttachedDebugInsts(&words);

// Predicates
inst->IsBranch();
inst->IsBlockTerminator();
inst->IsDecoration();
inst->IsConstant();
inst->IsLoad();
inst->IsNop();
inst->ToNop();        // turns instruction into OpNop

BasicBlock

for (auto& block : func) {  // func is a Function&
  uint32_t label = block.id();
  Instruction* label_inst = block.GetLabelInst();
  Instruction* merge = block.GetMergeInst();       // OpSelectionMerge / OpLoopMerge
  Instruction* loop_merge = block.GetLoopMergeInst();
  bool has_phi = block.HasPhiInstructions();

  // Iterate instructions (label included if you use ForEachInst)
  for (auto& inst : block) { }
  block.ForEachInst([](Instruction* i){ }, true);  // true = include debug lines

  // Terminator helpers
  block.ForEachSuccessorLabel([](uint32_t* id){ });  // NOTE: callback takes uint32_t*
  bool is_loop_header = block.IsLoopHeader();
  uint32_t merge_id = block.MergeBlockIdIfAny();
  uint32_t continue_id = block.ContinueBlockIdIfAny();
  Instruction* term = block.terminator();
}

Function

for (auto& func : *get_module()) {  // func is a Function&
  uint32_t func_id = func.DefInst().result_id();
  bool is_declaration = func.IsDeclaration();
  func.ForEachParam([](Instruction* param){ });
  // iterate blocks (func is a Function& from the module loop)
  for (auto& block : func) { }
}

Function does not have an IsEntryPoint() method. Check entry points via the module:

bool IsEntryPoint(Function* func, Module* module) {
  for (auto& entry : module->entry_points()) {
    // OpEntryPoint: operand 0 = execution model, operand 1 = function id
    if (entry.GetSingleWordInOperand(1) == func->result_id()) return true;
  }
  return false;
}

Building Instructions

#include "source/opt/ir_builder.h"

// Insert before an instruction
InstructionBuilder b(context(), insertion_point,
  IRContext::kAnalysisInstrToBlockMapping | IRContext::kAnalysisDefUse);

// Or append to end of a block
InstructionBuilder b(context(), parent_block,
  IRContext::kAnalysisInstrToBlockMapping | IRContext::kAnalysisDefUse);

Instruction* add = b.AddBinaryOp(type_id, spv::Op::OpIAdd, lhs, rhs);
Instruction* extract = b.AddCompositeExtract(elem_type_id, composite_id, {idx0, idx1});
Instruction* construct = b.AddCompositeConstruct(type_id, {id0, id1});
Instruction* load = b.AddLoad(type_id, ptr_id);
Instruction* store = b.AddStore(ptr_id, value_id);
Instruction* branch = b.AddBranch(target_id);
Instruction* cbranch = b.AddConditionalBranch(cond_id, true_id, false_id);
Instruction* cbranch_with_merge = b.AddConditionalBranch(cond_id, true_id, false_id, merge_id);
Instruction* phi = b.AddPhi(type_id, {val0, block0, val1, block1});
Instruction* unary = b.AddUnaryOp(type_id, spv::Op::OpConvertFToS, operand);
Instruction* nullary = b.AddNullaryOp(type_id, spv::Op::OpGroupAll);
Instruction* select = b.AddSelect(type_id, cond_id, true_id, false_id);
Instruction* access = b.AddAccessChain(ptr_type_id, base_ptr_id, {idx_id0, idx_id1});
Instruction* var = b.AddVariable(ptr_type_id, static_cast<uint32_t>(spv::StorageClass::Function));

InstructionBuilder can only preserve kAnalysisDefUse and kAnalysisInstrToBlockMapping; other analyses must be invalidated/rebuilt explicitly.

Replacing / Killing

// Replace all uses of old_id with new_id
ctx->ReplaceAllUsesWith(old_id, new_id);

// Replace uses only when predicate returns true
ctx->ReplaceAllUsesWithPredicate(old_id, new_id, [](Instruction* user) {
  return user->opcode() == spv::Op::OpStore;
});

// Kill an instruction (removes from block, updates def-use)
ctx->KillInst(inst);

// Kill an id and all its uses
ctx->KillDef(id);

// Get the defining instruction for an id
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);

// Get users of an id
ctx->get_def_use_mgr()->ForEachUser(id, [](Instruction* user){ });
uint32_t n = ctx->get_def_use_mgr()->NumUsers(id);

Safe Iteration & Phi Nodes

When deleting instructions while iterating, collect first and kill after:

std::vector<Instruction*> to_kill;
get_module()->ForEachInst([&](Instruction* inst) {
  if (inst->IsNop()) to_kill.push_back(inst);
}, false);
for (auto* inst : to_kill) context()->KillInst(inst);

Phi operands arrive as (value_id, parent_block_id) pairs:

if (inst->opcode() == spv::Op::OpPhi) {
  for (uint32_t i = 0; i + 1 < inst->NumInOperands(); i += 2) {
    uint32_t value = inst->GetSingleWordInOperand(i);
    uint32_t parent = inst->GetSingleWordInOperand(i + 1);
  }
}

Testing

Tests live in test/opt/. Use PassTest<::testing::Test> fixture.

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "test/opt/pass_fixture.h"
#include "test/opt/pass_utils.h"
#include "source/opt/my_pass.h"

using MyPassTest = PassTest<::testing::Test>;

TEST_F(MyPassTest, Basic) {
  const std::string before = R"(
OpCapability Shader
OpMemoryModel Logical GLSL450
%void = OpTypeVoid
%fn = OpTypeFunction %void
%main = OpFunction %void None %fn
%entry = OpLabel
OpReturn
OpFunctionEnd
)";
  const std::string after = before;
  SinglePassRunAndCheck<MyPass>(before, after, /*skip_nop=*/false, /*do_validation=*/false);
}

Fixture helpers:

  • SinglePassRunAndCheck<PassT>(before, after, skip_nop, do_validation, args...) — exact match.
  • SinglePassRunAndCheck<PassT>(before, after, skip_nop, args...) — overload without validation.
  • SinglePassRunAndMatch<PassT>(original, do_validation, args...) — runs pass, disassembles, then checks with Effcee CHECK: patterns embedded in original. Always skips OpNop. Returns std::tuple<std::string, Pass::Status>.
  • SinglePassRunAndFail<PassT>(original, args...) — expects Status::Failure, checks error messages with Effcee CHECK: patterns embedded in original.
  • SinglePassRunToBinary<PassT>(assembly, skip_nop, args...) — returns std::tuple<std::vector<uint32_t>, Pass::Status>.
  • SetAssembleOptions(SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS) — keep ids from assembly.
  • SetDisassembleOptions(SPV_BINARY_TO_TEXT_OPTION_NO_HEADER) — omit SPIR-V header in output.
  • SetTargetEnv(spv_target_env) — change target environment (default SPV_ENV_UNIVERSAL_1_3).

For match tests, embed CHECK: lines as comments in the assembly string:

const std::string assembly = R"(
; CHECK: OpReturn
OpCapability Shader
OpMemoryModel Logical GLSL450
%void = OpTypeVoid
%fn = OpTypeFunction %void
%main = OpFunction %void None %fn
%entry = OpLabel
OpReturn
OpFunctionEnd
)";
SinglePassRunAndMatch<MyPass>(assembly, false);

Multi-pass tests

TEST_F(MyPassTest, Pipeline) {
  const std::string before = R"(... )";
  const std::string after = R"(... )";
  AddPass<MyFirstPass>();
  AddPass<MySecondPass>();
  RunAndCheck(before, after);
}

Manual context tests

#include "source/opt/build_module.h"

TEST(MyPass, Manual) {
  std::unique_ptr<IRContext> ctx =
    BuildModule(SPV_ENV_UNIVERSAL_1_3, nullptr, assembly,
                SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS);
  ASSERT_NE(ctx, nullptr);
  MyPass pass;
  auto status = pass.Run(ctx.get());
  EXPECT_EQ(status, Pass::Status::SuccessWithChange);
  EXPECT_TRUE(ctx->IsConsistent());
}

Registering a Pass

  1. Add header to source/opt/passes.h (or include directly).
  2. Add CreateMyPassPass() factory declaration to include/spirv-tools/optimizer.hpp:
Optimizer::PassToken CreateMyPassPass();
  1. Implement factory in source/opt/optimizer.cpp:
Optimizer::PassToken CreateMyPassPass() {
  return MakeUnique<Optimizer::PassToken::Impl>(MakeUnique<opt::MyPass>());
}
  1. Add CLI flag mapping in source/opt/optimizer.cpp inside Optimizer::RegisterPassFromFlag:
} else if (pass_name == "my-pass") {
  RegisterPass(CreateMyPassPass());
  1. Add source files to source/opt/CMakeLists.txt:
  my_pass.h
  ...
  my_pass.cpp
  1. Add a test target/file under test/opt/ (e.g. my_pass_test.cpp) and list it in test/opt/CMakeLists.txt.

Look at nearby passes in RegisterPassFromFlag for the exact pattern. Passes with arguments parse pass_args before calling RegisterPass(...).

Analyses & Invalidation

Available IRContext::Analysis bits:

  • kAnalysisNone, kAnalysisDefUse, kAnalysisInstrToBlockMapping, kAnalysisDecorations
  • kAnalysisCombinators
  • kAnalysisCFG, kAnalysisDominatorAnalysis, kAnalysisLoopAnalysis
  • kAnalysisNameMap, kAnalysisScalarEvolution, kAnalysisRegisterPressure
  • kAnalysisValueNumberTable, kAnalysisStructuredCFG, kAnalysisBuiltinVarId
  • kAnalysisIdToFuncMapping, kAnalysisConstants, kAnalysisTypes
  • kAnalysisDebugInfo, kAnalysisLiveness, kAnalysisIdToGraphMapping

After Process() returns SuccessWithChange, Pass::Run() itself (not PassManager) automatically calls:

ctx->InvalidateAnalysesExceptFor(GetPreservedAnalyses());

If you mutate IDs outside normal helpers (e.g. CompactIdsPass), manually invalidate kAnalysisDebugInfo and any others that become stale mid-pass.

If you need an analysis inside Process() and are not preserving it, it is usually fine to request it via ctx->get_def_use_mgr() etc.; the manager will build it on demand. Just make sure GetPreservedAnalyses() reflects what survives your transformations.

Pass Manager / Recipes

spvtools::Optimizer opt(SPV_ENV_UNIVERSAL_1_3);
opt.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* msg) {
  std::cerr << msg << std::endl;
});
opt.RegisterPass(spvtools::CreateCompactIdsPass())
    .RegisterPass(spvtools::CreateAggressiveDCEPass());
opt.Run(binary.data(), binary.size(), &optimized);

Built-in recipes:

  • RegisterPerformancePasses() / RegisterSizePasses() / RegisterLegalizationPasses()
  • All three also have overloads taking a bool preserve_interface argument.

Repo Integration (LuisaCompute)

LuisaCompute consumes SPIRV-Tools as a vendored library and registers no custom Pass subclasses of its own — the "Registering a Pass" workflow above only applies when editing src/ext/SPIRV-Tools itself. The integration point is lc::spirv::optimize_spirv() in src/backends/common/spirv/spirv_codegen/optimizer.cpp (called from entry.cpp):

  • Builds a spvtools::Optimizer for SPV_ENV_VULKAN_1_2 and registers only built-in spvtools::Create*Pass() factories, then runs the pipeline in a fixed-point loop until the binary stops changing.
  • Presets (SpirvOptimizerOptions, default level 2 → compute): none; lightweight = adce, block-merge, simplification, dead-branch-elim; compute = register_compute_passes() (DXC-like pipeline; loop-unroll is registered only when the input already carries an explicit Unroll OpLoopMerge control bit); full = RegisterPerformancePasses() + private-to-local + copy-propagate-arrays + spread-volatile-semantics + compact-ids. trim-capabilities is appended only for modules within the audited capability allowlist (can_safely_trim_capabilities).
  • Env knobs read in the same file: LUISA_SPIRV_OPT_LEVEL (int), LUISA_SPIRV_OPT_PASSES (preset name), LUISA_SPIRV_OPT_PASS_FLAGS (comma/space-separated --pass[=arg] flags appended after the preset via Optimizer::RegisterPassesFromFlags; an invalid flag fails closed and keeps the input binary), LUISA_SPIRV_OPT_MAX_ITERATIONS (fixed-point cap, 1–10, default 5), LUISA_SPIRV_OPT_SROA_LIMIT (scalar-replacement limit, default 100, 0 = unlimited), LUISA_SPIRV_OPT_MAX_ID_BOUND, LUISA_SPIRV_OPT_PRESERVE_BINDINGS (1/true/0/false).
  • To exercise any upstream pass (or the whole registry) on a built runtime without code changes, set e.g. LUISA_SPIRV_OPT_PASS_FLAGS=--strength-reduction.

File Map

FilePurpose
source/opt/pass.h / pass.cppBase Pass class
source/opt/mem_pass.h / mem_pass.cppMemPass base for mem2reg-style passes
source/opt/empty_pass.h / null_pass.hNo-op passes for testing
source/opt/ir_context.h / ir_context.cppIRContext, analysis management
source/opt/module.hModule, header, section lists
source/opt/function.hFunction
source/opt/basic_block.hBasicBlock
source/opt/instruction.hInstruction, Operand, DebugScope
source/opt/ir_builder.hInstructionBuilder
source/opt/def_use_manager.hDefUseManager
source/opt/type_manager.hTypeManager
source/opt/constants.hConstantManager
source/opt/cfg.hCFG
source/opt/fold.hFolder (constant folding)
source/opt/passes.hUnified include for all pass headers
test/opt/pass_fixture.hPassTest fixture
include/spirv-tools/optimizer.hppPublic C++ API
source/opt/optimizer.cppPass factories & CLI flag dispatch
examples/cpp-interface/main.cppExample of assemble / validate / optimize / disassemble

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.