Skip to content

[uc] add activation, norm, pos emb, quant, sdpa operators for uc backend. - #342

Open
shengw-bd wants to merge 5 commits into
masterfrom
mazx/uc-kernel
Open

[uc] add activation, norm, pos emb, quant, sdpa operators for uc backend.#342
shengw-bd wants to merge 5 commits into
masterfrom
mazx/uc-kernel

Conversation

@shengw-bd

Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Claude Code Review

Verdict: Request changes -- New UC backend has a few correctness/perf issues (sync calls on hot paths, broken empty-input scale shape, mis-scoped backend import) that should be fixed before merge.

Summary

Adds a new "uc" (Unified Compiler) backend for Ascend NPU with operator implementations for activation, attention, gemm, normalization, position embedding, and quantization, plus README and registry updates. Also gates the ttx import in failures and adds an MOJO_DETERMINISTIC env tweak for NPU matmul.

Must fix

  • [BLOCKER] UC backend gated by torch_npu platform check (likely wrong / duplicate block) -- mojo_opset/backends/__init__.py:38-44 -- The new uc import is guarded by _SUPPORT_TORCH_NPU_PLATFROM and added in a second duplicated if block; this couples uc availability to torch_npu's platform set and produces two consecutive identical conditionals. Use a dedicated _SUPPORT_UC_PLATFORM (or platform=="npu") check and merge with the torch_npu block.
  • [BLOCKER] torch.npu.synchronize() on hot quant path -- mojo_opset/backends/uc/operators/quant.py:41 and :97 -- Forcing a device sync inside forward will serialize the stream and wreck decode/prefill performance. Remove the sync; if it is a workaround for a kernel-launch ordering bug, fix the kernel wrapper instead.
  • [BLOCKER] Empty-input scale shape is wrong for 1D input -- mojo_opset/backends/uc/operators/quant.py:17-19 and :84-86 -- torch.empty(*input.shape[:-1], 1, ...) produces a 1D tensor of length 1 when input is 1D, but the non-empty branch returns shape (1, 1) from kernel_scale.reshape(*input.shape[:-1], 1) only because _matrix_shape promotes to (1, N). The two branches disagree; align the empty path with the non-empty contract (and MojoDynamicQuant's spec).
  • [BLOCKER] MOJO_DETERMINISTIC writes a global env var unconditionally on import -- mojo_opset/backends/__init__.py:46-48 -- Setting os.environ["CLOSE_MATMUL_K_SHIFT"]="1" at import time leaks into every process that touches mojo_opset and is order-dependent vs. CANN initialization. Move this into a backend-specific init/setup hook, or at minimum only set if unset and document it.
  • [BLOCKER] _typed_api membership check is buggy/expensive -- mojo_opset/backends/uc/operators/_utils.py:34-38 -- kernels.keys() with in works but kernels.load() returning a non-dict could silently fail; more importantly, the fp16-fallback path returns api (no suffix) which depends on a kernel naming convention not enforced anywhere. Validate that kernels is dict-like and document/assert the naming contract, or always require the suffixed name.

Suggestions

Suggestions (6)
  • [MAJOR] Per-row epsilon tensor is wasteful -- mojo_opset/backends/uc/operators/normalization.py:21,55,108,148 -- Allocating a (rows,) fp32 tensor full of eps on every forward is unnecessary; pass eps as a scalar to the kernel or cache the tensor.
  • [MAJOR] Python loop over batch for varlen position ids -- mojo_opset/backends/uc/operators/position_embedding.py:79-90 -- _varlen_position_ids iterates with .item() per request, causing host-device syncs per batch element on the prefill path. Vectorize with torch.repeat_interleave / torch.arange ops.
  • [MAJOR] Silent fallback in UCSdpa hides kernel availability -- mojo_opset/backends/uc/operators/attention.py:88-92 -- Catching NotImplementedError from _typed_api and silently dispatching to torch is reasonable, but log at debug/once so missing kernels aren't invisible during perf triage.
  • [MAJOR] DTensor = () sentinel is fragile -- mojo_opset/backends/uc/operators/gemm.py:9-11 -- isinstance(tensor, ()) raises TypeError. Use DTensor = None and guard with DTensor is not None and isinstance(...).
  • [MINOR] Repeated .contiguous() of static weights -- mojo_opset/backends/uc/operators/normalization.py:25,63,116,156 -- self.weight.contiguous() / self.bias.contiguous() runs every forward; cache once at init or assert contiguous.
  • [MINOR] kernels.keys() is redundant -- mojo_opset/backends/uc/operators/_utils.py:35,37 and gemm.py:22 -- in kernels is sufficient and clearer if kernels is a dict.

Nits

Nits (3)
  • [NIT] Typo _SUPPORT_TORCH_NPU_PLATFROM is pre-existing but propagated; consider fixing while touching this file -- mojo_opset/backends/__init__.py:29.
  • [NIT] UCResidualAddRMSNorm.forward returns (empty, empty) aliasing the same tensor on the empty path -- mojo_opset/backends/uc/operators/normalization.py:46-48 -- callers may mutate one and surprise the other.
  • [NIT] MojoApplyPenaltiesTempurate typo retained in README table; not introduced here but visible in the diff -- README.md:135.

Notes

  • [CHECK] UCResidualAddRMSNorm returns (output, output) when norm_pos != "pre" -- confirm this matches the base class contract; aliasing the output as the residual seems suspicious -- mojo_opset/backends/uc/operators/normalization.py:67-69.
  • [CHECK] Backend priority places uc after torch for npu -- mojo_opset/core/backend_registry.py:14 -- if uc is intended to take precedence over the generic torch fallback on Ascend, reorder; otherwise it will rarely be selected.
  • [CHECK] UCApplyRoPE._flatten_cos_sin requires fp32 cos/sin -- mojo_opset/backends/uc/operators/position_embedding.py:194-195 -- verify upstream callers always supply fp32 (the rotary cache dtype here is self.cos.dtype, not necessarily fp32).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new Unified Compiler (UC) based backend for Ascend NPU kernels, adding support for various operators including activation, attention, GEMM, normalization, position embedding, and quantization, along with documentation and registry updates. The review feedback highlights several performance and robustness improvements, such as checking for uc_kernel availability at import time, eliminating redundant CPU-NPU synchronizations in quantization and position embedding operators, safer handling of DTensor operations in GEMM, and minor code cleanups like removing redundant code blocks and using idiomatic dictionary membership checks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1 to +5
from mojo_opset.utils.platform import get_impl_by_platform

_op_map = get_impl_by_platform()
globals().update(_op_map)
__all__ = list(_op_map.keys())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The uc backend will be imported and registered even if uc_kernel is not installed, because uc_kernel is only imported lazily at runtime. If the uc backend is selected or fell back to, it will crash at runtime with an ImportError. Checking for uc_kernel availability at import time allows the backend to be gracefully skipped and logged as a warning.

Suggested change
from mojo_opset.utils.platform import get_impl_by_platform
_op_map = get_impl_by_platform()
globals().update(_op_map)
__all__ = list(_op_map.keys())
import importlib.util
if importlib.util.find_spec("uc_kernel") is None:
raise ImportError("uc_kernel is not installed, which is required by the uc backend.")
from mojo_opset.utils.platform import get_impl_by_platform
_op_map = get_impl_by_platform()
globals().update(_op_map)
__all__ = list(_op_map.keys())

Comment on lines +81 to +97
def _varlen_position_ids(
x: torch.Tensor,
cu_q_lens: torch.Tensor,
total_seq_lens: Optional[torch.Tensor],
) -> torch.Tensor:
position_ids = torch.empty((x.shape[0],), device=x.device, dtype=torch.int32)
q_lens = cu_q_lens[1:] - cu_q_lens[:-1]
for i in range(q_lens.numel()):
q_len = q_lens[i].item()
context_len = 0 if total_seq_lens is None else total_seq_lens[i].item() - q_len
position_ids[cu_q_lens[i]:cu_q_lens[i + 1]] = torch.arange(
context_len,
context_len + q_len,
device=cu_q_lens.device,
dtype=torch.int32,
)
return position_ids

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Calling .item() inside a loop over sequence lengths causes multiple CPU-GPU/NPU synchronizations, which is a major performance bottleneck in the hot path of LLM execution. Slicing with NPU tensors also adds overhead. Copying cu_q_lens and total_seq_lens to CPU once using .tolist() and performing the loop and slicing using CPU integers completely eliminates these synchronizations and slicing overhead.

    @staticmethod
    def _varlen_position_ids(
        x: torch.Tensor,
        cu_q_lens: torch.Tensor,
        total_seq_lens: Optional[torch.Tensor],
    ) -> torch.Tensor:
        position_ids = torch.empty((x.shape[0],), device=x.device, dtype=torch.int32)
        cu_q_lens_cpu = cu_q_lens.cpu().tolist()
        total_seq_lens_cpu = total_seq_lens.cpu().tolist() if total_seq_lens is not None else None

        for i in range(len(cu_q_lens_cpu) - 1):
            start, end = cu_q_lens_cpu[i], cu_q_lens_cpu[i + 1]
            q_len = end - start
            context_len = 0 if total_seq_lens_cpu is None else total_seq_lens_cpu[i] - q_len
            position_ids[start:end] = torch.arange(
                context_len,
                context_len + q_len,
                device=x.device,
                dtype=torch.int32,
            )
        return position_ids

Comment on lines +41 to +42
torch.npu.synchronize()
return kernel_output.reshape(input.shape), kernel_scale.reshape(*input.shape[:-1], 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Calling torch.npu.synchronize() forces CPU-NPU synchronization, which breaks the asynchronous execution pipeline of PyTorch and introduces massive CPU bubbles. Since PyTorch's stream semantics naturally guarantee that subsequent operations on the output tensors will wait for the kernel to finish, this synchronization is completely redundant and should be removed.

Suggested change
torch.npu.synchronize()
return kernel_output.reshape(input.shape), kernel_scale.reshape(*input.shape[:-1], 1)
return kernel_output.reshape(input.shape), kernel_scale.reshape(*input.shape[:-1], 1)

Comment on lines +98 to +99
torch.npu.synchronize()
return kernel_output.reshape(input.shape), kernel_scale.reshape(*input.shape[:-1], 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Calling torch.npu.synchronize() forces CPU-NPU synchronization, which breaks the asynchronous execution pipeline of PyTorch and introduces massive CPU bubbles. Since PyTorch's stream semantics naturally guarantee that subsequent operations on the output tensors will wait for the kernel to finish, this synchronization is completely redundant and should be removed.

Suggested change
torch.npu.synchronize()
return kernel_output.reshape(input.shape), kernel_scale.reshape(*input.shape[:-1], 1)
return kernel_output.reshape(input.shape), kernel_scale.reshape(*input.shape[:-1], 1)

Comment thread mojo_opset/backends/__init__.py Outdated
Comment on lines +44 to +48
if platform == "npu" and get_bool_env("MOJO_DETERMINISTIC", default=False):
import os

# special setting for npu deterministic matmul
os.environ["CLOSE_MATMUL_K_SHIFT"] = "1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This block of code is completely redundant. Setting CLOSE_MATMUL_K_SHIFT when MOJO_DETERMINISTIC is enabled is already handled at lines 11-17. Removing this redundant block keeps the code clean and maintainable.

Comment on lines +35 to +38
if typed_api in kernels.keys():
return typed_api
if dtype == torch.float16 and api in kernels.keys():
return api

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Python, checking membership directly in a dictionary (e.g., key in dict) is more idiomatic and significantly faster than calling .keys() (e.g., key in dict.keys()), which creates an unnecessary view object.

Suggested change
if typed_api in kernels.keys():
return typed_api
if dtype == torch.float16 and api in kernels.keys():
return api
if typed_api in kernels:
return typed_api
if dtype == torch.float16 and api in kernels:
return api

Comment on lines +43 to +54
if self.trans_weight:
weight = self.weight.t().contiguous()
else:
weight = self.weight
weight = _to_local_tensor(weight)
input = _to_local_tensor(input)
input_scale = _to_local_tensor(input_scale).flatten().float().contiguous()
weight_scale = _to_local_tensor(self.weight_scale).flatten().float().contiguous()
if not input.is_contiguous():
input = input.contiguous()
if not weight.is_contiguous():
weight = weight.contiguous()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calling .t().contiguous() on a DTensor before converting it to a local tensor can be extremely inefficient or fail depending on the PyTorch version. It is much safer and more efficient to convert the DTensor to a local tensor first using _to_local_tensor, and then perform transpose and contiguous operations locally. This also avoids redundant .contiguous() calls.

Suggested change
if self.trans_weight:
weight = self.weight.t().contiguous()
else:
weight = self.weight
weight = _to_local_tensor(weight)
input = _to_local_tensor(input)
input_scale = _to_local_tensor(input_scale).flatten().float().contiguous()
weight_scale = _to_local_tensor(self.weight_scale).flatten().float().contiguous()
if not input.is_contiguous():
input = input.contiguous()
if not weight.is_contiguous():
weight = weight.contiguous()
input = _to_local_tensor(input)
weight = _to_local_tensor(self.weight)
if self.trans_weight:
weight = weight.t()
input_scale = _to_local_tensor(input_scale).flatten().float().contiguous()
weight_scale = _to_local_tensor(self.weight_scale).flatten().float().contiguous()
if not input.is_contiguous():
input = input.contiguous()
if not weight.is_contiguous():
weight = weight.contiguous()

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Claude Code Review

Verdict: Request changes -- New UC backend has several correctness/perf concerns that should be addressed before merging.

Summary

This PR introduces a new "uc" (Unified Compiler) backend for Ascend NPU, wiring it into the platform backend registry and providing initial UC implementations for activation, attention (SDPA), GEMM, normalization, RoPE, and dynamic quant ops. It also updates the README support matrix and gracefully handles import failures of optional backends.

Must fix

  • [BLOCKER] UC backend priority below torch -- mojo_opset/core/backend_registry.py:14 -- "uc" is appended after "torch" in the npu priority list, so the UC kernels will never be picked when torch native implementations exist. Place "uc" before "torch" (and likely before "xops") so opting into MOJO_BACKEND="uc" actually selects UC kernels.
  • [BLOCKER] MoE dynamic quant double work / wrong scaling -- mojo_opset/backends/uc/operators/quant.py:74-83 -- You apply inv_smooth_scale in float32 on the host via repeat_interleave+multiply, then call mojo_dynamic_quant with an all-ones smooth scale. This (a) defeats the purpose of using the kernel, (b) incurs an extra full-tensor cast/copy on every forward, and (c) silently differs in numerics from a fused path. Pass the per-expert smooth scale through to the kernel (or call a real MoE-aware kernel) instead of pre-scaling on Python.
  • [BLOCKER] UCResidualAddRMSNorm/LayerNorm post-norm path returns wrong residual -- mojo_opset/backends/uc/operators/normalization.py:57-60, 106-109 -- When norm_pos != "pre" you return (output, output), discarding the kernel-produced kernel_residual_output. The second tuple element is supposed to be the updated residual (hidden+residual), not the normalized output; this will corrupt downstream residual streams in post-norm models. Return updated_residual (or whatever the core contract specifies) instead of output.
  • [BLOCKER] _typed_api membership check on .keys() -- mojo_opset/backends/uc/operators/_utils.py:35, gemm.py:18 -- typed_api in kernels.keys() works only if kernels is a dict; if it's a custom registry you may get false negatives, and even for dicts in kernels is the idiomatic form. More importantly, this is on every kernel call (hot path) -- cache the resolved callable per (api, dtype) instead of doing a string format + dict lookup each forward.
  • [BLOCKER] Broad except Exception masks ttx import bugs -- mojo_opset/backends/__init__.py:30-34 -- Catching Exception (not ImportError) for the ttx backend will swallow real bugs (e.g. a NameError or RuntimeError raised at import time) and silently disable the backend with only a warning. Narrow to ImportError like you correctly did for the uc backend at line 41.

Suggestions

Suggestions (6)
  • [MAJOR] UCSdpa kernel is single-shape only -- mojo_opset/backends/uc/operators/attention.py:16-18,53-58 -- Hard-coding one (1,5,1,4096,128) shape and raising NotImplementedError otherwise will make MojoSdpa unusable on uc unless that exact shape is hit. Consider routing unsupported shapes to super().forward(...) (as you do for masked inputs) rather than raising, so uc selection is safe.
  • [MAJOR] UCApplyRoPE limited to 3 hard-coded configs -- mojo_opset/backends/uc/operators/position_embedding.py:127-133, 200-206 -- Same concern: _STATIC_APPLY_ROPE_CONFIGS is a tiny allow-list; everything else raises. A graceful fallback to the parent implementation would make this less of a footgun.
  • [MAJOR] run_kernel returns None and is order-sensitive -- mojo_opset/backends/uc/operators/_utils.py:65-67 -- It just forwards *args positionally; every caller has to know the exact kernel ABI. A short docstring describing that the kernel writes to output tensors in-place (and which positions are outputs) would prevent future misuse.
  • [MAJOR] lru_cache(maxsize=1) on _uc_kernels hides import errors -- mojo_opset/backends/uc/operators/_utils.py:14-18 -- If uc_kernel.load() raises, the failure happens lazily on first op call, not at backend import, so the warning at backends/__init__.py:44 won't fire. Consider eagerly loading at module import (or at least surfacing the error more clearly).
  • [MINOR] MoEDynamicQuant host-side .item() calls -- mojo_opset/backends/uc/operators/quant.py:65, 70 -- torch.any(... < 0).item() and token_count.sum().item() force device-host syncs every forward. Either drop the validation in production paths or guard it behind a debug flag.
  • [MINOR] kernel_output.reshape(x.shape) after empty_like(kernel_input) -- mojo_opset/backends/uc/operators/_utils.py:48 -- kernel_input is already contiguous with the same numel as x but possibly different shape only if x was non-contiguous; the reshape is fine but will not preserve x's strides. Document that output is always contiguous.

Nits

Nits (3)
  • [NIT] _SUPPORT_TTX_PLATFROM typo is preserved -- mojo_opset/backends/__init__.py:19 -- new _SUPPORT_UC_PLATFORM is spelled correctly; consider fixing the existing typo while touching this file.
  • [NIT] _OUTPUT_DTYPE_SUFFIX in gemm.py duplicates _DTYPE_API_SUFFIX in _utils.py -- mojo_opset/backends/uc/operators/gemm.py:9 -- consider importing the shared map.
  • [NIT] Empty-input fast paths use torch.empty_like which leaves uninitialized memory -- e.g. activation.py via _utils.py:42, attention.py:79 -- prefer torch.zeros_like or document that callers won't read empty results.

Notes

  • [CHECK] MojoResidualAdd*Norm contract: I assumed the second tuple element is the updated residual regardless of norm_pos. Please verify against the core class and the existing ttx/torch_npu implementations.
  • [CHECK] UCRotaryEmbedding._arange_cache requires x.shape[1] to be seq_len regardless of head_first; confirm this matches the parent class's expected layout.
  • [CHECK] MojoMoEDynamicQuant -- the parent class's exact forward signature and whether inv_smooth_scale is per-expert [E, C] or per-feature [C] should be re-validated; the repeat_interleave assumes [E, C].

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

Claude Code Review

Verdict: Request changes -- New UC backend has several correctness concerns in MoE quant, ApplyRoPE shape handling, and registry priority placement.

Summary

Adds a new "uc" (Unified Compiler) backend for Ascend NPU, registering it in the platform priority list and providing kernels for activation, attention (SDPA), gemm, normalization, position embedding, and quantization. Most ops route through a thin run_kernel/run_unary_kernel/run_binary_kernel shim that dispatches to dtype-suffixed UC kernel artifacts.

Must fix

  • [BLOCKER] uc backend priority placed last -- mojo_opset/core/backend_registry.py:14 -- "uc" appears after "torch", so it will never be selected over torch fallbacks on npu. If uc is meant to be a real NPU kernel backend, place it ahead of torch (and likely after/before torch_npu per intent).
  • [BLOCKER] MoE dynamic quant pre-scales then re-quantizes via plain dynamic kernel -- mojo_opset/backends/uc/operators/quant.py:117-130 -- multiplying inputs by inv_smooth_scale in float and casting back to input dtype, then running mojo_dynamic_quant with an all-ones smooth scale, both loses precision and is not a per-expert smooth-quant operation. This will produce incorrect scales/outputs vs. the reference MoE quant; either implement a proper per-expert kernel or fall back to super().
  • [BLOCKER] ApplyRoPE 4D head_first reshape uses wrong dims for k -- mojo_opset/backends/uc/operators/position_embedding.py:158-160 -- after transpose(1,2).contiguous(), q/k are [B, S, H, D]; using k.shape[0]*k.shape[1] works only if k's B and S match q, but the code earlier asserts only q.ndim == k.ndim, not that batch/seq match. Add explicit asserts that q/k share batch and seq, and reshape using the same batch_size, seq_len you computed for q to avoid silent miscompute when shapes diverge.
  • [BLOCKER] ResidualAddRMS/LayerNorm post-norm path returns (output, output) -- mojo_opset/backends/uc/operators/normalization.py:67-69, 121-123 -- when norm_pos != "pre", the second return value should be the updated residual (input + residual), not the normed output. Returning output twice will corrupt subsequent residual-add chains. Verify against MojoResidualAddRMSNorm semantics and return updated_residual (or the appropriate tensor) in both branches.
  • [BLOCKER] _typed_api membership check uses .keys() on possibly non-dict -- mojo_opset/backends/uc/operators/_utils.py:35, gemm.py:18 -- if typed_api in kernels.keys() assumes uc_kernel.load() returns a dict; if it returns a registry object without .keys() this raises AttributeError. Use in kernels or check the API explicitly via the loader's documented interface.

Suggestions

Suggestions (6)
  • [MAJOR] Silent fallback on ttx import failure -- mojo_opset/backends/__init__.py:30-34 -- changing from .ttx import * to a broad except Exception hides real bugs in the ttx backend on supported platforms; at minimum log the traceback or restrict to ImportError like the uc branch.
  • [MAJOR] UCSdpa hardcoded shape gating -- mojo_opset/backends/uc/operators/attention.py:17-19,46-55 -- only one static shape is supported and everything else raises NotImplementedError; consider falling back to super().forward for unsupported shapes so registry selection does not break models.
  • [MAJOR] inv_smooth_scale cast to float32 may diverge from reference -- mojo_opset/backends/uc/operators/quant.py:62-64 -- MojoDynamicQuant likely keeps inv_smooth_scale in input dtype; verify converting to fp32 here matches reference numerics on bf16 paths.
  • [MAJOR] _matrix_shape collapses all leading dims -- mojo_opset/backends/uc/operators/_utils.py:22-26 -- treating any >2D tensor as (prod(leading), last) is fine for elementwise/RMS but is wrong if any kernel expects true 2D contiguous strides for non-contiguous inputs of higher rank; the .contiguous() call covers it but worth a comment.
  • [MINOR] warning_once may not exist on standard logger -- mojo_opset/backends/uc/operators/attention.py:84, position_embedding.py:48 -- confirm get_logger returns a logger that supports warning_once; otherwise this raises AttributeError on the fallback path.
  • [MINOR] lru_cache(maxsize=1) on kernel loader is process-global -- mojo_opset/backends/uc/operators/_utils.py:14-18 -- fine, but if uc_kernel.load() is expensive and not thread-safe, consider an explicit lock.

Nits

Nits (3)
  • [NIT] _OUTPUT_DTYPE_SUFFIX in gemm.py:8 duplicates _DTYPE_API_SUFFIX in _utils.py:7 -- consolidate.
  • [NIT] mojo_opset/backends/uc/operators/__init__.py is empty; either populate exports or add a comment.
  • [NIT] position_embedding.py:74 raises AssertionError manually -- prefer ValueError for runtime input validation.

Notes

  • [CHECK] MojoResidualAddRMSNorm.norm_pos semantics -- confirm what the second return value should be in pre vs post mode; current uc impl differs from typical "pre returns updated residual, post returns hidden+residual" patterns.
  • [CHECK] setdefault("CLOSE_MATMUL_K_SHIFT", "1") change is a behavioral relaxation -- ensure existing deterministic-mode users were not relying on the unconditional override.
  • [CHECK] UC SDPA fallback to super().forward(query, key, value, attn_mask) -- verify base MojoSdpa.forward accepts these positional args and enable_gqa/scale are read from self.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

Claude Code Review

Verdict: Request changes -- New UC NPU backend has several correctness/UX issues that should be fixed before merge.

Summary

Adds a new "uc" (Unified Compiler) backend for Ascend NPU, exposing kernels for activation, attention (SDPA), gemm, normalization, RoPE, and quantization, plus README/registry updates. Backend is registered with the lowest priority on npu and import failures are handled gracefully.

Must fix

  • [BLOCKER] Backend priority places uc last, after torch -- mojo_opset/core/backend_registry.py:14 -- With "torch" ahead of "uc", the UC kernels will never be selected on npu since torch native is always available. Move "uc" ahead of "torch" (and likely ahead of or near torch_npu/ttx per intent).
  • [BLOCKER] UCResidualAddRMSNorm/UCResidualAddLayerNorm post-norm path returns (output, output) -- mojo_opset/backends/uc/operators/normalization.py:60-62, 109-111 -- For norm_pos != "pre" you discard updated_residual and return the normalized output as the residual, which is almost certainly wrong. Return (output, updated_residual) (or whatever the reference op returns) and add a test.
  • [BLOCKER] UCMoEDynamicQuant ignores token_count semantics and double-applies smooth scale -- mojo_opset/backends/uc/operators/quant.py:117-133 -- It builds expanded_inv_smooth_scale per-token but then calls mojo_dynamic_quant again with dynamic_inv_smooth_scale = ones. The pre-multiplication into kernel_input.dtype loses precision and the per-expert scale is applied in fp arithmetic rather than fused. Either implement a real MoE kernel or fall back to super().forward; do not silently ship a degraded path.
  • [BLOCKER] UCApplyRoPE requires fp32 cos/sin but MojoRotaryEmbedding likely produces input-dtype cos/sin -- mojo_opset/backends/uc/operators/position_embedding.py:177-179 -- Chaining UCRotaryEmbedding -> UCApplyRoPE will raise NotImplementedError whenever the cache dtype is not fp32. Verify the cache dtype contract or relax/cast inside the op.
  • [BLOCKER] _typed_api membership check uses kernels.keys() and may misbehave -- mojo_opset/backends/uc/operators/_utils.py:36, gemm.py:18 -- Use in kernels (membership on the mapping) rather than in kernels.keys(); current form works for dicts but suggests kernels may not be a dict (it's uc_kernel.load()). If it returns a non-dict, .keys() may not exist or may not support in. Confirm and use a robust check (e.g. hasattr / getattr(kernels, api, None)).

Suggestions

Suggestions (6)
  • [MAJOR] Broad except Exception on ttx import -- mojo_opset/backends/__init__.py:30-34 -- Was previously unguarded; swallowing all exceptions can hide real bugs. Narrow to ImportError like the uc branch.
  • [MAJOR] UCSdpa mask path warns then falls back, but only for non-None mask -- mojo_opset/backends/uc/operators/attention.py:80-85 -- The static-shape kernel is extremely narrow (one shape only). Consider falling back to super().forward for any unsupported shape rather than raising NotImplementedError, to keep the backend usable.
  • [MAJOR] UCQuantGemm ignores bias if base op has one -- mojo_opset/backends/uc/operators/gemm.py:27-58 -- MojoQuantGemm typically supports an optional bias and output scale; verify and either pass them through or assert they are absent.
  • [MAJOR] _matrix_shape collapses all leading dims -- mojo_opset/backends/uc/operators/_utils.py:23-27 -- Fine for elementwise ops, but used in normalization where the kernel is per-row over hidden size. Confirm the kernel expects cols == hidden_size (i.e. last dim) for all callers; for layernorm with normalized_shape of rank > 1 this would be wrong.
  • [MINOR] Duplicate _OUTPUT_DTYPE_SUFFIX and _DTYPE_API_SUFFIX -- mojo_opset/backends/uc/operators/gemm.py:8-12 vs _utils.py:6-10 -- Consolidate into one mapping.
  • [MINOR] os.environ.setdefault("CLOSE_MATMUL_K_SHIFT", "1") is a behavior change -- mojo_opset/backends/__init__.py:15-16 -- Prior code unconditionally set "1"; users who set "0" will now get different behavior. Intentional, but call out in changelog.

Nits

Nits (4)
  • [NIT] _uc_kernels, _typed_api, _matrix_shape are imported by name across modules despite leading underscore -- mojo_opset/backends/uc/operators/_utils.py -- drop the underscore or expose a public re-export.
  • [NIT] UCRotaryEmbedding.__init__ requires init_max_length but base allows None -- position_embedding.py:21-29 -- document this in the class docstring.
  • [NIT] _assert_* helpers use bare assert -- attention.py:30-55 -- these vanish under python -O; prefer explicit raise ValueError.
  • [NIT] Trailing newline-only __init__.py -- mojo_opset/backends/uc/operators/__init__.py:1 -- can be empty.

Notes

  • [CHECK] Confirm uc_kernel.load() returns a dict-like object supporting both [name] indexing and name in obj; otherwise _typed_api and _require_kernel will fail at runtime.
  • [CHECK] Verify the README support-matrix entries (uc_npu columns) match what the backend actually registers; e.g. MojoStaticQuant is marked supported but only int8 is implemented.
  • [CHECK] UCApplyRoPE._STATIC_APPLY_ROPE_KERNELS hard-codes a small allowlist; ensure callers in modeling code don't hit unsupported (head_dim, rope_dim, dtype) tuples in production paths.

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.

2 participants