Performance Tuning
Performance Tuning Process
With the growing significance of computing, parallel computing devices such as graphics processing units (GPUs) and neural network processing units (NPUs) play an increasingly important role in artificial intelligence and other industries. Computing efficiency, also called computing performance, has attracted increasing attention.
This document introduces the concepts of performance, performance tools, and methods for tuning the performance of training models on Ascend devices. Figure 1 shows the performance tuning process.
Figure 1 Performance tuning process

Performance Metrics
Align the following key metrics:
- Single-card and multi-card scenarios: In single-card scenario verification, partition CPU resources in Docker according to the customer configuration for verification. In multi-card scenarios, CPU-bound issues may occur. During early debugging, confirm that CPU resources meet requirements.
- Latency requirements: Confirm whether a fixed or variable value is required, and confirm the requirement standards for low-load and high-load scenarios.
- Batch size distribution: Confirm whether a fixed or dynamic value is required and the batch size distribution.
- Queries per second (QPS): The number of requests is the total number of requests within an interval. QPS = requests per second (req/sec).
- Throughput: It is the maximum amount of sample data that the network model can process per unit time, for example, 1 s. Throughput = QPS × batch size.
- Data types: float16, float32, and other types.
Introduction to Performance Tuning Tools
msProf
PyTorch-Specific Performance Profiling
For PyTorch, you are advised to use the profiling interface. After profiling, the tool automatically parses the performance data. For details, see msprof common profiling commands in the CANN Performance Tuning Tool User Guide.
The following is a complete example:
import torch
import torch_npu
device = torch.device("npu")
class DemoModel(torch.nn.Module):
def forward(self, in0, in1):
mul0 = in0 * in1
sub0 = mul0 ** 2 - in0
sub1 = mul0 ** 2 - in1
slice0 = sub0[:, :2]
slice1 = sub1[:, 2:]
cat0 = torch.cat([slice0, slice1], dim=1)
return cat0
def main():
torch.manual_seed(2025)
in0 = torch.randn(3,5).to(device)
in1 = torch.randn(3,5).to(device)
model = DemoModel().to(device)
output = model(in0, in1)
print(output.shape)
print(output.device)
print(output)
# perf code
experimental_config = torch_npu.profiler._ExperimentalConfig(
export_type=[
torch_npu.profiler.ExportType.Text,
torch_npu.profiler.ExportType.Db
],
profiler_level=torch_npu.profiler.ProfilerLevel.Level1,
msprof_tx=False,
aic_metrics=torch_npu.profiler.AiCMetrics.AiCoreNone,
l2_cache=False,
op_attr=False,
data_simplification=False,
record_op_args=False,
gc_detect_threshold=None
)
steps = 10
with torch_npu.profiler.profile(
activities=[
torch_npu.profiler.ProfilerActivity.CPU,
torch_npu.profiler.ProfilerActivity.NPU
],
schedule=torch_npu.profiler.schedule(wait=3, warmup=0, active=1, repeat=1, skip_first=1),
on_trace_ready=torch_npu.profiler.tensorboard_trace_handler("./result"),
record_shapes=False,
profile_memory=False,
with_stack=False,
with_modules=False,
with_flops=False,
experimental_config=experimental_config) as prof:
print("profiling start...")
for step in range(steps):
output = model(in0, in1)
prof.step()
if __name__ == "__main__":
main()
Performance profiling for TensorFlow and PyTorch
In TensorFlow, no interface can be called directly. You must use the msprof command for profiling. Use dynamic profiling to control the volume of collected data. For details, see Dynamic Profiling of Performance Data in the CANN Performance Tuning Tool User Guide.
The sample code is as follows:
import npu_device
from npu_device.compat.v1.npu_init import *
import numpy as np
import tensorflow as tf
tf.compat.v1.disable_eager_execution()
session_config = tf.compat.v1.ConfigProto()
custom_op = session_config.graph_options.rewrite_options.custom_optimizers.add()
custom_op.name = "NpuOptimizer"
custom_op.parameter_map["graph_max_parallel_model_num"].i = 1
custom_op.parameter_map["aicore_num"].s = tf.compat.as_bytes("7|10")
session_config.graph_options.rewrite_options.remapping = RewriterConfig.OFF
left_shape = [1, 8000]
right_shape = [800, 1]
x = tf.compat.v1.placeholder(tf.int64, shape=left_shape)
y = tf.compat.v1.placeholder(tf.int64, shape=right_shape)
equal_ret = tf.math.equal(x, y)
inputs_x = np.random.rand(*left_shape)
inputs_x = inputs_x.astype(np.int64)
inputs_y = np.random.rand(*right_shape)
inputs_y = inputs_y.astype(np.int64)
with tf.compat.v1.Session(config=session_config) as sess:
for i in range(100000):
result = sess.run(equal_ret, feed_dict={x:inputs_x, y:inputs_y})
print(result)
-
Run inference in a loop. Shortly after the model starts inference, manually obtain the PID of the running program. For example, if the PID is 9527, run the following dynamic profiling command to collect data:
msprof --dynamic=on --pid=9527 --output=/home/projects/output --model-execution=on --runtime-api=on --aicpu=on > start ... > stop ... > quitThe period after the
startcommand is the dynamic profiling time window, which ends when you enter thestopcommand. -
After the data is collected, parse it manually. Enter the directory collected in the preceding step (usually a directory with a timestamp) and run the following commands to parse the data:
// Enable parsing and output profiling to the current directory. msprof --parse=on --output=./ // Enable export and save the results in CSV format to the current directory. msprof --export=on --output=. --summary-format=csvNote
Excessive profiling time will lead to long parsing time. Properly control the profiling time. Generally, you can perform data analysis with 5 seconds of profiling.
Graph Engine Dump
Execution graph analysis must be based on the graph optimized by the graph engine (GE), so you must dump it by configuring the relevant environment variables.
The environment variables involved are DUMP_GE_GRAPH, DUMP_GRAPH_LEVEL, and DUMP_GRAPH_PATH. For details, see Graph Compilation in the CANN Environment Variable Reference.
Common configurations are DUMP_GE_GRAPH=2 and DUMP_GRAPH_LEVEL=2.
In the dumped graph folder, several .pbtxt and .pb files are generated in sequence after each stage of the graph optimization process completes. For example, in ge_onnx_00000101_graph_0_Build.pbtxt, 00000101 is the sequence number. The 0 in graph_0 represents the rank ID, which is always 0 in recommendation inference scenarios. The build graph here corresponds to the graph in the execution stage. Analyze the optimization space through profiling and the network structure corresponding to this graph.
msIT Tool: Dump the GE graph, and use the msit graph function of the tool to scan for repeated structures. For substructures that appear many times and account for a large proportion, consider manually writing fused passes and fused operators for optimization. The tool also has a subgraph extraction function. For example, in scenarios where the graph is too large to open, you can extract a subgraph for analysis. A third-party network visualization tool is recommended: netron.app.
Performance Optimization Analysis
The software stack involved is primarily PyTorch and TensorFlow. In these two scenarios, different frameworks may be derived according to the actual application scenarios. This section introduces the basic flow of this software stack.
PyTorch Technology Stack

TensorFlow Technology Stack

Bring-up
When a customer model is started, the specific use of the TensorFlow or PyTorch technology stack usually depends on the customer's needs. The choice of the bring-up software stack should be based on the customer's inference framework process.
- For the TensorFlow framework, the customer usually provides a .pb file. The model can be run according to the customer's software stack. You can refer to the community demo (TF Inference Sample Reference).
- For the PyTorch route, you can use the TorchAir suite for inference. Refer to the community demo (TorchAir Inference Sample).
- For the ecosystem route, use the Inductor + Triton process. After completing the bring-up, you can initially observe the performance baseline and compare it with the target, while referring to the profiling methods in previous sections for subsequent analysis.
Different Bottlenecks
Several ideal scenarios are listed below for analysis. In reality, most cases involve solving one bound only for another to appear, or scenarios where they are mixed. If no bound appears, NPU utilization may not be saturated. You can first construct a high-load scenario (such as a local pressure test script, running the model with multiple streams and high load) and then analyze the bottlenecks.
Cube/Vector
Program performance is dominated by the execution time of the computing cores (kernels). You can check whether the program is bound through the following points:
-
Use
npu-smi infoto check utilization. If utilization is very high, the program is likely bound. Utilization may be relatively low during single inference but exceed 80% (generally considered very high) during parallel inferences. This value is the ratio of cycles on the Cube core converted from the clock frequency and does not include the proportion of Vector cores. This is a preliminary check and specifics must be calculated from the profiling cycles.Calculate utilization through profiling, as shown in the following figure.

-
For example, filtering
AI_COREhere calculates the utilization ofCUBE. -
Sort by start time. Calculating the difference between the first and last timestamps gives the total time for the entire segment.
-
The total number of
CUBEcycles is the sum ofaic_total_cycles. -
Utilization = ③/(②/1,000,000 × 20 × 1,650 × 1,000,000). The unit of ② is µs, so it is divided by 1,000,000 to convert to s. 20 is the number of AI Cores on the specific chip. 1,650 is the frequency of the current chip in MHz/s, so it is multiplied by 1,000,000.
The utilization calculated here is 78.9% of the overall Cube utilization.
-
The proportion of each process in the calculation can be calculated directly using time. For example, if
mte2time accounts for 70.18% of the total AI Core time, memory input during the AI Core execution accounts for the majority.
Optimization methods in Cube/Vector-bound scenarios:
- Graph optimization, such as
torch.compileor TensorFlow XLA. - Operator fusion to reduce operator launch overhead and intermediate reading/writing.
- Data types, such as reducing from float32 to HF32. For the same data, the computing workload decreases (this may affect accuracy, so perform accuracy testing). Note that the HF32 data type only takes effect for Conv and Matmul operators.
Host
Analysis process
When opening the profiling timeline file through the chrome://tracing/ page, you can see operator dispatch similar to the following.

As shown in the figure, there are many "bubbles" on the device, and the computing power is not fully utilized.
This situation is generally seen in dynamic graph scenarios or single-operator dispatch scenarios, where processing must complete on the host before a single operator is dispatched, and the operator execution time is less than the host-side processing time.
For host-side computational workload, export data through the native TensorFlow Profiler to check the proportion of host and device time and judge by CPU utilization. After hybrid computing is enabled (refer to: www.hiascend.com), the system automatically keeps operators that cannot be executed on the device to be executed on the host. You can also specify certain operators not to offload to the device, leading to higher host calculation volume. Use the native TensorFlow Profiler to capture the performance of the entire session run (refer to www.tensorflow.org) and analyze the proportion of host-side and device-side time.
Solutions
- Replace single-operator dispatch schemes with full-graph offloading.
- Convert dynamic graphs to static graphs.
- Increase the batch size to increase operator execution time and reduce the proportion of free gaps.
- Improve host-side performance. If CPU utilization is too high, consider CPU-side optimizations.
- Deploy in a cluster to ensure sufficient host-side resources.
- For single-machine deployment, consider mixed deployment of non-host-bound models and host-bound models.
PCIe
The simplest way to analyze this is to increase the load. PCIe time becomes significantly longer while other times do not change much.
The time consumed by PCIe in the timeline file is shown as model@inputcopy in the figure.

Actual rate indicators are summarized in the pcie_*.csv file through summary information. For details, see www.hiascend.com. The relevant variable is --sys-interconnection-profiling. For example, Tx_p_avg in the figure is the input data, with an average rate of 99.628 MB/s.

Theoretical data calculation: Assuming PCIe 5.0, the bidirectional transmission rate of each PCIe lane is close to 8 GB/s. A card typically has an x4 interface, so the theoretical maximum rate is close to 32 GB/s. The actual rate varies according to the size of the transmitted file. Generally, a maximum of 80% of the theoretical bandwidth can be reached. The actual bandwidth is strongly related to the shape and size of the sent data.
Analysis direction
- Determine how many cards correspond to one CPU and whether time consumption caused by PCIe contention can be reduced through operations such as NUMA node binding.
- Increase the batch size to reduce the proportion of PCIe transmission header overhead.
- Data format: Reducing from float32 to float16 can also effectively solve the issue. Ensure that accuracy meets the requirement or is acceptable to the customer.
- Accumulate data on the host first, converting small data transmissions into large ones. The GE framework has this function. If the customer calls the
aclrtinterface, they must implement it themselves.
Memory Access
This section mainly targets memory input and output of Vector or Cube operators, where memory access becomes a bottleneck.
-
Generally, the memory access rate can be calculated through the profiling file according to the data transfer volume of each operator and its transfer time to judge whether a memory access bound exists.

As shown in the preceding figure, the input data volume is 20,000/48 × 2 × 8 = 6.5 KB. 48 indicates that data is distributed to 48 cores, 2 indicates two shapes of 20,000, 8 indicates
int64, andmte2time is 1051 µs, so the calculated bandwidth is 0.005 GB/s. This bandwidth is far below the theoretical bandwidth and is problematic. -
Directly judge whether the
aiv_mte2_ratiofield in the profiling file exceeds 0.8. If many operators exceed 0.8, this part must be optimized, as shown in the figure.
Analysis direction
- Analyze whether the time consumption is reasonable.
- Vector operator fusion can effectively reduce the amount of data to be copied, releasing memory access pressure.
- Check whether the amount of data transferred is 32-byte aligned. Non-aligned scenarios lead to low bandwidth and require special processing within the operator.
Case Sharing 1
Background
A customer allocates a fixed percentage of traffic from a large pool to a cluster. A certain number of NPUs in this cluster handle these requests. The goal is to reduce the number of NPUs in the cluster while ensuring that request latency does not exceed the threshold.
Analysis process
Local test data -> step 1:
-
Batching strategy is implemented in the customer's server-side framework. Batching time and levels can be modified through parameters.
-
Maximum BS level means that each batch size from 1 to this maximum level is configured as a level.
-
Number of streams represents the number of instances on one NPU, that is, the maximum number of inference tasks that can run in parallel.
-
For core partitioning, the first number is the number of Cube cores, and the second is the number of Vector cores.
-
Performance in the last three columns is the QPS value (1,000/average time × number of streams).

The table shows that QPS is highest when the maximum batch size level is 16 and the number of streams is 3. Note that this data is from a local uniform pressure test, and the batch size distribution does not represent real online conditions. It serves only as a directional analysis method.
Online batch size distribution -> step 2:
Batch size data captured from the online environment is shown in the figure. Most batch sizes are small: 90% are below 20, and 86% are below 16.

Based on local test performance, we can choose a maximum batch size level of 16 and 3 streams.
Online batch size performance tuning -> step 3:
Actual data shows that during peak traffic, NPU latency deteriorates severely. Latency increases as traffic increases, so utilization is suspected.
Comparing local test data in the 12/24 core partitioning scenario, NPU processing for a request with a batch size of 20 is as follows.

- If the maximum batch size is 20, the request completes in 9.19 ms, occupying 12/24 cores.
- If the maximum batch size is 16, the request is split into two tasks of batch size 10. They complete in parallel in 7.01 ms. The time is shorter than batch size 20, but the occupied hardware resources are 24/48 cores, which is double that of batch size 20, leading to excessively high NPU utilization.
After the maximum batch size is changed, actual average online latency showed no obvious change, but NPU utilization dropped by 5% on average, and latency deterioration during peak traffic improved significantly.
Case Sharing 2
Background
For a coarse ranking model, TP99 latency under low load must be reduced to within 5 ms while maintaining core limits (Cube: 7 cores, Vector: 10 cores) to ensure performance in high-load scenarios.
Analysis process
Profiling:
Since the goal is to optimize low-pressure latency, profiling is performed serially. Run the model for inference, then use msprof --dynamic=on --pid=9527 --output=/home/projects/output --model-execution=on --runtime-api=on --aicpu=on for dynamic profiling. For details, see msProf.
Bring-up: In the scenario without optimization (only operator mode configuration, GatherV2 high performance, Cube HF32 enabled), a single modelExecute takes 51 ms, which is 10 times the target.

-
Due to serial execution, operator time consumption is stable. For profiling, analyze the performance of a single inference after deduplication based on
op_name.
-
Sorting by
task_durationshows that each inference takes around 44 ms. Due to many dynamic operators, atask_waittime of around 7 ms (waiting for host scheduling) is introduced.Analyzing the
Gatheroperator shape and structure: the dynamicGatheroperator uses logical cores and is not limited by the core limiting function of GE. This violates the prerequisite for high-load performance. The index number of theGatheroperator is at most 20,000. Theoretical analysis suggests it should not take ms-level time (high data redundancy causes large inter-core conflicts).Structure analysis shows that this
Gathercomes from the firstGatherin the following structure.
This substructure introduces the
Whereoperator, so subsequent operator shapes are not fixed, introducing dynamic subgraphs. SinceWhereis a type-3 operator (the output shape is not fixed), the dynamic graph must return shape information to the host afterWhereexecution before subsequent shape derivation. This leads to hightask_waittime.Execution logic analysis shows that this substructure extends the
Gatherlogic to return an all-zero vector for index < 0. Based on repeated data analysis, the input carries long segments of padded zeros. Implementing a customGatheroperator and loading part of the table into UB solves the dynamic subgraph, core limiting, and performance issues.
Profiling analysis --> step 2:
After solving these issues through custom operators, collect profiling data again. The serial execution time dropped to around 9 ms, and the performance of default_gather improved significantly.


Operators not replaced by DefaultGather are not in that dynamic structure. However, for a Gather operator with a sequence length of 4,000, theoretical analysis shows that it should not take more than 180 µs. The actual issue is caused by the GatherV2 high-performance mode in the full-load scenario. For a table shape of 400 x 50, performance improves greatly after full loading.
A custom Gather operator is used here to cover this scenario (synchronous operator optimization requirement).
Profiling analysis --> step 3:
After the remaining Gather operators are replaced with CustomGather (full-load logic), the serial time reached around 6.1 ms. You can now scan the model for optimization points comprehensively.

Analyzing bottlenecks: the top ones are TopK and Equal. The underlying TopK operator does not support SIMT and is implemented through complex algorithms on Vector, leading to long latency. There is no optimization space in the operator itself. However, the TopK input contains padded zeros. If effective length is passed to TopK, average execution time can be significantly reduced (little impact on TP99 but affects maximum QPS). This depends on upper-layer framework changes and is not implemented here.
For the Equal operator, with shapes (1 x 4000) and (400 x 1), execution takes over 230 µs, far from the theoretical performance of Vector operators. This is caused by the Int64 implementation. Testing shows that an Int32 Equal with the same shapes takes less than 30 µs. Int64 should not take an order of magnitude more time. For this dual-broadcast scenario, an Int64 implementation of Equal was designed, reducing latency to around 75 µs.
AICPU operators such as Unique and Where were also found.
Operators depend on upper-layer graph splitting, which is controlled by the user. The Where operator comes from the following structure. The dynamic structure it introduces can be eliminated through a custom pass.

For this kind of BatchMatMul, replacing the calculation logic with 2, 400, 300 * 2, 300, 16 also improves performance.

After using custom Equal, eliminating Where, and replacing BatchMatMul, the serial latency reached 4.6 ms, meeting requirements.

Performance Tuning Methods
AICPU Operator Elimination
Background
Due to SIMD, the performance of AICPU operators on the NPU is generally poor. These operators must be eliminated or offloaded to the CPU.
Constraints
Currently, these are implemented through manual identification and modification of customer model code, requiring customer cooperation.
Specific cases
Case 1:
For TopK, int32/int64 values can only be executed on AICPU. If the type is fp32, there is an AI Vector implementation. If int32/int64-to-fp32 conversion is possible without significant precision loss (loss value < 2^24), TopK with integer indexes can be converted to cast(int32->fp32/int64) + TopK(fp32) + cast(fp32->int32/int64) through a custom pass.
Case 2:
For AICPU operators on the NPU without Vector implementations, if upper-layer operators are hard to offload to the CPU, consider graph modification so AICPU operators do not depend on NPU operators. As shown in the figure, the Gather operator that Bucketize depends on has indexes from a large NPU subgraph. This subgraph cannot be entirely offloaded. To offload Bucketize to the CPU, analyze that it only directly depends on Gather. As an index-type operator, Gather can have Bucketize performed on its table rather than its results (assuming table size is not much greater than output size). Moving Bucketize above the table branch of Gather shows that the Squeeze operator has no sequential dependency with Bucketize, so Bucketize can be moved above the input to be executed on the CPU.

Custom Passes
Specific cases
Case 1: BatchMatMul + Tile
The BatchMatMul operator has broadcast logic. If surrounding operators implement the same logic, they can be eliminated to improve performance. Deleting the Tile operator provides:
-
Time saved from the
Tileoperator itself. -
Saved memory access (128 × 8 × 300 × 32 × 4 B= 37.5 MB) introduced by
Tile, which affects cache hit rates of other instances. -
Faster
mtespeed for theBatchMatMuloperator as broadcasting is more efficient.
Case 2: homogeneous concat
In this scenario, using concat during graph construction implements the same function as Tile. However, the data input volume for Tile is only 1/240 of concat. If the tail axis is not aligned, concat also suffers performance loss.

Case 3: Broadcast BatchMatMul Operator
The original BatchMatMul operator performs matrix multiplication on (1, 32) and (300, 32). On NPU hardware, it only utilizes 1/16 of the Cube matrix unit. Since the first axis has broadcasting, shrinking it onto the k-axis replaces 128 separate (1, 32) × (300, 32) matrix multiplications with 8 larger (128, 32) × (300, 32) operations, improving Cube utilization. The introduced Reshape operator is not executed on the NPU, and the impact of the Transpose operator is outweighed by the improvement in calculation efficiency.

Case 4: Tile + concat
Swapping the execution order of Tile and concat reduces read memory access from 1 × 32 × 3 + 128 × 32 × 3 to 1 × 32 × 3 + 1 × 96 (98.5% decrease). Write memory access is reduced from 128 × 32 × 3 + 128 × 96 to 1 × 96 + 128 × 96 (50% decrease).

Custom Operators
Background
If performance issues cannot be avoided by optimizing structures through passes, manual optimization of operators is required. To ensure generalization, hand-written operators often require different tiling branches for different shapes, which involves large amounts of code. When optimizing models, excessive time spent may not meet user needs. Therefore, implementing optimization branches for specific network structures based on prior model knowledge is the optimal strategy.
Specific cases
Case 1: TopK with effective data length
TopK is used in recommendation inference (for example, the TWINS model). Even if TopK is optimized to the Vector core (the cast scheme), performance is poor for very long sequence lengths. In addition, due to hardware reasons, TopK algorithm performance on NPUs struggles to match that on GPUs. Therefore, optimization of the custom TopK operator needs to be analyzed at the model level. From the model perspective, TopK input parameters come from a padded sequence. Not all data participates in TopK. Effective data is only about 10% on average. Based on this prior condition, implementing a custom TopK operator with an effective length input parameter can reduce operator time by 90% on average.
Case 2: Custom FloorMod operator
In the model, certain features are bucketed through FloorMod. The logic is that index 0 is mapped directly to 0, while other indexes are bucketed by computing their modulo value plus 1, which is then passed to the Gather operator. Since NPU hardware lacks native support for int64 operations, the FloorMod operator resorts to scalar instructions when handling int64 types, causing significant performance degradation with large input volumes. While converting int64 to dual int32 operations is theoretically possible, it introduces substantial complexity in computation logic and sign bit manipulation. However, leveraging the structural constraints of this design, specifically the requirement that indexes be positive for Gather to function, a key optimization can be made. Since FloorMod is only used for indexing in Gather operations, the divisor, or number of buckets, does not need to be stored as int64. The underlying fmod and division instructions natively support only fp32 and fp16. Analysis shows that converting the divisor to fp32 incurs no precision loss for values up to 2^24-1, approximately 16.77 million, which exceeds practical requirements. Algorithm constraints typically limit bucket counts to 2^21-1 for performance optimization, a threshold sufficient for most real-world applications. Therefore, the operator can be designed to support int64/fp32 operations, leveraging existing vectorized implementations that circumvent the scalar instruction bottleneck.

Case 3: Custom Gather operator
For features with default values, a structure converts indexes < 0 to zero vectors and indexes > 0 to embeddings. The Where operator acts as an AICPU operator, and the structure executes it as a dynamic subgraph, resulting in poor performance. Integrating this logic into the Gather operator, judging index values inside the operator and moving zero vectors or embedding values, reduces overall time by 3 orders of magnitude.

Multi-stream
Background
Recommendation inference models often feature many operators with small shapes. They require high throughput within reasonable single inference time (data volume of a single inference task × number of inference tasks per unit time). Small shapes lead to low computing power utilization. Enabling multi-stream parallel inference improves utilization and throughput.
Constraints
- Multi-stream parallel scenarios introduce resource contention involving bandwidth and compute resources. This causes individual inference latency to increase compared to single-stream execution. For latency-sensitive applications, appropriate latency control is required.
- Without core partitioning, multi-stream processing may suffer from severe compute resource queuing. Whether to combine multi-stream with core partitioning should be determined based on actual data.
- The number of streams should be tuned according to actual throughput characteristics of the model. While more streams increase NPU utilization and boost overall throughput, exceeding an optimal threshold introduces cross-stream cache invalidation effects, which can reduce overall throughput performance.
Specific cases
- TensorFlow demo: Refer to GitCode.com for enabling multi-stream functionality.
- PyTorch demo: Since the PyTorch community lacks direct multi-stream support, use multi-processing for inference.
Core Partitioning
Background
Take the NPUs on the A2 server as an example. One NPU contains 24 AI Cube cores and 48 AI Vector cores. When an operator is executed, the number of cores required is calculated based on the shape. Generally, all cores are used. Using more cores reduces workload per core and operator time.
For CTR inference with multi-instance concurrency, consider the total resources occupied (number of cores) of operators in addition to the time consumed. If a Cube operator takes 100 µs and occupies 24 cores, the NPU can only execute that one operator. If the operator is limited to 12 cores, it takes 150 µs. This allows executing two such operators in 150 µs (average 75 µs), which is better.
The benefits of core limiting are as follows:
-
For microsecond-level small operators, much of the time is launch overhead. More cores increase this overhead.
-
In CTR scenarios, shapes are small. After core partitioning, mini-tiling within a core may be reduced, minimizing computing power waste in tail blocks.
-
Accessing the same address between NPU cores causes memory access conflicts. For index-type operators like
Gather, more cores exacerbate inter-core conflicts if data distribution is concentrated.The number of cores should not be limited too low. Generally, Vector/Cube limits should be within 6 to 12 cores. (Because Vector and Cube are different resources in the model, the 1:2 ratio introduced by hardware does not need to be ensured.)
Constraints
In single-stream scenarios, inference latency following core partitioning typically shows degradation compared to cases without core partitioning. For latency-sensitive applications, carefully control the extent of latency degradation based on latency requirements.
Specific cases
- TensorFlow demo: For modification instructions, see Session Configuration Parameters in the TensorFlow Adapter Interface (1.x) documentation to configure the
aicore_numparameter. - PyTorch demo: This feature is not supported currently.
Batch size Optimization
Background
For certain small operators, increasing batch size can reduce the proportion of overhead. It may also increase the number of cores utilized by these operators, thereby enhancing overall performance. This enables more efficient utilization of NPU core resources and reduces redundant weight transfers.
Constraints
- Increasing the batch size requires more cores for operator computation. When combined with core partitioning, this can result in significant latency degradation. Conduct thorough latency testing to determine the optimal batch size.
- The cache hit rate will decline.
- The batch size is typically dictated by the customer's service requirements. While you can test performance improvements with larger batch sizes in local benchmarking scripts, production performance may differ. Since batch size can vary and padding may be necessary in production environments, real-world optimal performance may not match local test results.
Specific cases
The table shows real model data where throughput increases with the batch size.
Table 1 Model data
| batchsize | Inference Time (ms) | Throughput |
|---|---|---|
| 86 | 8.5 | 10117 |
| 128 | 10.4 | 12258 |
| 256 | 14.9 | 17231 |
| 512 | 26.7 | 19195 |
Performance Tuning Cases
Host Side
Background
Using the Recsys-gr generative recommendation model as an example, end-to-end training performance differs significantly across CPU environments with different architectures (x86/Arm) and clock frequencies. After comparing the two hardware environments, we found that the device-side specifications are the same. After collecting profiling files, device-side time consumption shows no obvious difference.
The core difference between the two runtime environments lies in the CPU architecture specifications and clock frequency. End-to-end performance degrades severely in the low-frequency Arm environment. Therefore, a host-bound bottleneck is initially identified.
Optimization methods
Host-bound issues are mainly caused by two factors: operator dispatch latency and excessive CPU compute load. You can tune performance through the following methods:
Core Pinning (Process Affinity Binding)
Bind the training main process and data loading process to specific CPU cores to avoid frequent cross-core migration and context switching. This greatly reduces system scheduling overhead, accelerates host-side operator scheduling and task dispatch, and reduces NPU idle waiting time.
Enable Huge Pages
Enable the system huge page mechanism to reduce memory page table address translation overhead and page fault interruption overhead. This improves host memory access bandwidth, accelerates CPU memory allocation and data copy speed between the host and the NPU, and reduces host-side memory and communication time.
BIOS Tuning
Disable CPU power-saving frequency scaling policies, lock the CPU at a high clock frequency, and enable performance scheduling mode. This improves single-core CPU performance and speeds up host tasks such as serial operator scheduling and logical preprocessing, allowing CPU task dispatch to keep pace with NPU computation.
For detailed analysis, optimization methods, and cases for host-bound issues, see How to Locate and Resolve Host Bound Issues.
For performance tuning based on the Recsys-gr model, see the Recsys-gr Performance Tuning Example section.
Device Side
Background
In training and inference scenarios, the host can quickly complete task dispatch, while the NPU, or device side, handles core execution work such as operator computation and tensor operations. If device-side computation time is far greater than host-side task dispatch time, NPU compute resources remain fully loaded, the computation chain becomes blocked, and device-side bottlenecks become the core performance issue.
Optimization methods
Operator performance issues require specialized analysis tools and code optimization techniques. Collect profiling data for the model and compare metrics such as computation time and memory usage for different operators. You can optimize device-side operators through the following methods:
Affinity Operator Replacement
Replace PyTorch native general-purpose operators with NPU-specific affinity operators. These operators are deeply adapted to the NPU hardware architecture and compute cores, eliminate operator adaptation overhead, and greatly improve single-operator execution efficiency.
Fused Operators
Merge multiple consecutive fine-grained operators into one fused operator to reduce NPU kernel invocation overhead and memory read/write overhead for intermediate tensors. This simplifies the computation flow and avoids wasting NPU compute resources.
For detailed operator optimization strategies and cases, see NPU Affinity Adaptation Optimization and Operator Performance Issue Optimization Solution.
Communication
Background
In multi-card distributed training scenarios, the collective communication time spent on gradient aggregation, parameter synchronization, and data exchange between NPU cards accounts for too much of the total time and far exceeds the compute time of a single card. All cards end up waiting for communication to complete, and overall training throughput is dragged down by the communication stage. This is a communication bottleneck.
Optimization methods
Communication bottlenecks must be solved through distributed scheduling, HCCL communication library tuning, and load balancing. By adjusting execution logic and communication strategies, you can eliminate synchronization waiting time. The main optimization methods are as follows:
Computation-Communication Fusion
Break the serial execution model of computation and communication. Through gradient accumulation, pipeline parallelism, and gradient shard synchronization, the NPU can perform communication in the background while executing computation tasks, completely hiding communication time in computation gaps and offsetting communication wait overhead.
HCCL Communication Library Tuning
Tune parameters of the HCCL communication library and adjust features such as link and NIC configuration and buffer size to improve data transfer bandwidth and synchronization efficiency across multiple machines and cards, reducing collective communication latency.
Fast and Slow Card Issue Resolution
A fast card is a card that finishes its computation task first in the cluster. A slow card finishes its computation task more slowly in the cluster. Different cards finish tasks at different times, which causes fast cards to wait for slow cards and degrades the performance of the entire cluster.
For fast and slow card issues caused by NPU card performance differences and uneven node load in the cluster, use hardware resource isolation, load-balanced data sharding, and unified scheduling of cards with the same specifications to equalize iteration time across cards. This removes synchronization blocking caused by fast cards waiting for slow cards and avoids the barrel effect.
For detailed communication optimization issues and cases, see Communication Issue Optimization Solution and Communication Optimization.
Throughput
Background
In recommendation inference scenarios, model inference efficiency is limited by instance resource contention and insufficient scheduling density. The number of inference tasks that can be processed per unit time, or throughput, is low. Overall inference performance cannot be fully unleashed, making this a key bottleneck for efficient inference services.
Optimization methods
Throughput bottlenecks must be solved through resource scheduling, hardware core limiting, and batch scheduling strategies. By allocating resources properly and improving scheduling density, you can maximize throughput. The main optimization methods are as follows:
Multi-Instance Parallelism
Deploy multiple inference instances to make full use of idle hardware resources, improve the parallel processing capability of inference tasks, break through the processing limit of a single instance, directly expand the number of inference tasks that can be handled per unit time, and improve overall throughput.
AI Core Core Limiting (Core Partitioning)
For hardware resource contention that often occurs during multi-instance parallelism, control the allocation of AI Core resources to avoid resource preemption conflicts between instances. This ensures stable operation of each inference instance, effectively improves throughput, and reduces inference latency.
Optimize Scheduling Density
Increase the inference batch size appropriately to improve task scheduling density, reduce scheduling gap overhead, fully leverage hardware compute power, and improve throughput while balancing throughput and latency within a controllable range.
For detailed throughput optimization issues and cases, see Inference Tuning Cases.