Skip to content

Commit 1eb4396

Browse files
jimburtoftUbuntu
andauthored
Fix Neuron engine for SDK 2.28+ and add Trainium (trn2) support (#659)
* Fix Neuron engine compatibility with optimum >= 2.0 and Neuron SDK 2.28 - Handle removal of optimum.bettertransformer in optimum >= 2.0 by wrapping the import in try/except (acceleration.py) - Include token_type_ids in Neuron tokenizer output to match compiled model expectations (neuron.py) - Update Dockerfile.neuron to remove pinned SDK versions - Rewrite README.md with tested instructions for AWS DLAMI setup Tested on inf2.xlarge with Neuron SDK 2.28, optimum-neuron 0.4.3 and 0.4.5. Performance: ~210 embeddings/sec throughput, ~25ms latency (bge-small-en-v1.5). * Add CHECK_OPTIMUM guard and cross-instance benchmarks - Fix NameError when BetterTransformerManager is not available in check_if_bettertransformer_possible() (affects both torch and neuron engines on optimum >= 2.0) - Update README with benchmark results across g5, inf2, trn2 instances * Use data parallelism instead of tensor parallelism for Neuron Compile models for a single NeuronCore (num_cores=1) instead of sharding across all cores. This gives 5% better single-core performance and enables linear scaling via multiple processes. With 2 processes on inf2.xlarge (2 cores): - Before (tensor parallel): 206 emb/s - After (data parallel): 425 emb/s (+106%) Update README with multi-process deployment instructions and revised benchmark results. * Add trn2.3xlarge benchmarks and data parallelism examples to Neuron docs - Add trn2.3xlarge (4 NeuronCores) benchmark results: 19ms latency, 753 emb/s - Add trn2 data parallelism example (4 processes on 4 cores) - Restructure throughput table to show scaling across instance types - Note: trn2 has ~30% lower per-core latency than inf2 * Address code review feedback: fix BetterTransformer guard, add NEURON_NUM_CORES env var - acceleration.py: Use globals() check instead of CHECK_OPTIMUM.is_available to correctly detect when bettertransformer import failed (mark_dirty does not invalidate the cached is_available property) - neuron.py: Add NEURON_NUM_CORES env var (default 1) so large models can opt in to tensor parallelism without source changes - neuron.py: Remove dead get_nc_count() function and unused imports - Dockerfile: Merge RUN layers, add --no-cache-dir, add tested version comment - README: Clean up redundant text in throughput table --------- Co-authored-by: Ubuntu <ubuntu@ip-172-31-42-254.sa-east-1.compute.internal>
1 parent 12defd7 commit 1eb4396

4 files changed

Lines changed: 171 additions & 83 deletions

File tree

infra/aws_neuron/Dockerfile.neuron

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
# Is an mirror of
2-
# 763104351884.dkr.ecr.us-west-2.amazonaws.com/huggingface-pytorch-inference-neuronx:2.1.2-transformers4.43.2-neuronx-py310-sdk2.20.0-ubuntu20.04
1+
# Recommended: Use the AWS Deep Learning AMI Neuron (Ubuntu 24.04) directly
2+
# instead of building a custom Docker image. See README.md for instructions.
3+
#
4+
# If you must use Docker, this Dockerfile provides a starting point.
5+
# Note: The Neuron runtime must be available on the host (--device=/dev/neuron0).
6+
7+
# Base image with Neuron SDK pre-installed
8+
# Mirror of HuggingFace Neuron inference image
39
FROM michaelf34/aws-neuron-base-img:0.0.25-inference AS base
410

511
WORKDIR /app
612

713
COPY ./infra/aws_neuron/requirements_no_gpu.txt requirements_no_gpu.txt
8-
RUN pip3 install -r requirements_no_gpu.txt
14+
RUN pip3 install --no-cache-dir -r requirements_no_gpu.txt
915
RUN pip config set global.extra-index-url https://pip.repos.neuron.amazonaws.com
10-
# req
11-
# RUN pip3 install --no-deps --upgrade optimum[neuronx]==1.20.0
12-
RUN pip3 install --no-deps sentence_transformers==3.3.1
13-
# libneuronxla-2.0.5347.0 ml-dtypes-0.2.0 neuronx-cc-2.15.143.0+e39249ad setuptools-69.5.1 torch-neuronx-2.1.2.2.3.2 torch-xla-2.1.5 transformers-neuronx-0.12.313
14-
RUN pip3 install --upgrade neuronx-cc==2.15.* torch-neuronx torchvision transformers-neuronx libneuronxla protobuf optimum-neuron==0.0.20
16+
# Tested with: optimum-neuron 0.4.4, optimum 2.0.0, neuronx-cc 2.21, torch-neuronx 2.8
17+
RUN pip3 install --no-cache-dir --no-deps sentence_transformers && \
18+
pip3 install --no-cache-dir --upgrade neuronx-cc torch-neuronx torchvision libneuronxla protobuf optimum-neuron optimum
1519

16-
# base is also checkpointed to
17-
# docker pull michaelf34/aws-neuron-base-img:neuroncc2-15--optimum-1-17--transformers-4-36
1820
FROM base AS infinity_latest
1921
COPY ./libs/infinity_emb .
2022
RUN pip3 install -e .
2123
ENV INFINITY_BATCH_SIZE=8
2224
ENV INFINITY_ENGINE=neuron
23-
ENTRYPOINT [ "infinity_emb" ]
25+
ENTRYPOINT [ "infinity_emb" ]

infra/aws_neuron/README.md

Lines changed: 134 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,146 @@
1-
# Launch an EC2 Instance on AWS:
1+
# Running Infinity on AWS Inferentia / Trainium
22

3-
### Start a EC2 Instance with Huggingface AMI (free AMI image with Neuron Tools/Docker installed)
4-
- https://aws.amazon.com/marketplace/pp/prodview-gr3e6yiscria2
5-
- View Purchase Options -> Configure
6-
- Use `64-Bit AMI`, `20241115 (Nov 18, 2024)`
7-
- Region, e.g. `us-west-2`
8-
- Set Instance type `inf2.xlarge` (has two neuron accelerators)
9-
- Login with username `ubuntu` (using your standard EC2 setup e.g. `ssh ubuntu@ec2-14-11-13-12.us-west-2.compute.amazonaws.com`)
3+
## Recommended: Use the HuggingFace Neuron AMI (no Docker)
4+
5+
The simplest approach is to run Infinity directly on an EC2 instance with the
6+
HuggingFace Neuron AMI, which comes with `optimum-neuron`, `optimum`, `transformers`,
7+
and `sentence-transformers` pre-installed with compatible Neuron SDK versions.
8+
9+
### 1. Launch an EC2 Instance
10+
11+
- Use the **HuggingFace Neuron AMI** (`huggingface-neuron-*`) from the AWS Marketplace
12+
- This AMI ships optimum-neuron 0.4.4, neuronx-cc 2.21, Python 3.10 — all compatible
13+
- Search for `huggingface-neuron` in the EC2 AMI catalog
14+
- Instance type: **inf2.xlarge** (2 NeuronCores, 32 GB), **trn2.3xlarge** (4 NeuronCores, 128 GB), or larger
15+
- Disk: The AMI defaults to 512 GB
16+
17+
### 2. Install Infinity
18+
19+
```bash
20+
# SSH into the instance
21+
ssh ubuntu@<your-instance-ip>
22+
23+
# Activate the pre-installed PyTorch environment
24+
source /opt/aws_neuronx_venv_pytorch_2_8/bin/activate
25+
26+
# Clone and install Infinity from source (don't overwrite Neuron packages)
27+
git clone https://github.com/michaelfeil/infinity.git ~/infinity
28+
cd ~/infinity/libs/infinity_emb
29+
pip install --no-deps .
30+
31+
# Install remaining runtime dependencies (most are already present on the HF AMI)
32+
pip install uvicorn fastapi orjson typer httptools pydantic posthog \
33+
prometheus-fastapi-instrumentator hf_transfer rich
34+
```
35+
36+
### 3. Run Infinity with Neuron engine
37+
38+
```bash
39+
# Single core (uses one NeuronCore)
40+
infinity_emb v2 --engine neuron --model-id BAAI/bge-small-en-v1.5 --batch-size 4
41+
```
42+
43+
The first run will compile the model for Neuron (~100 seconds). Subsequent runs use the cached compilation.
44+
45+
### 4. Scale across all NeuronCores (data parallelism)
46+
47+
The Neuron runtime is limited to one model per process. To use all NeuronCores,
48+
run one server process per core, each pinned to a different core:
49+
50+
```bash
51+
# inf2.xlarge has 2 NeuronCores (cores 0 and 1)
52+
NEURON_RT_VISIBLE_CORES=0 infinity_emb v2 --engine neuron --model-id BAAI/bge-small-en-v1.5 --batch-size 4 --port 7997 &
53+
NEURON_RT_VISIBLE_CORES=1 infinity_emb v2 --engine neuron --model-id BAAI/bge-small-en-v1.5 --batch-size 4 --port 7998 &
54+
55+
# trn2.3xlarge has 4 NeuronCores (cores 0-3)
56+
NEURON_RT_VISIBLE_CORES=0 infinity_emb v2 --engine neuron --model-id BAAI/bge-small-en-v1.5 --batch-size 4 --port 7997 &
57+
NEURON_RT_VISIBLE_CORES=1 infinity_emb v2 --engine neuron --model-id BAAI/bge-small-en-v1.5 --batch-size 4 --port 7998 &
58+
NEURON_RT_VISIBLE_CORES=2 infinity_emb v2 --engine neuron --model-id BAAI/bge-small-en-v1.5 --batch-size 4 --port 7999 &
59+
NEURON_RT_VISIBLE_CORES=3 infinity_emb v2 --engine neuron --model-id BAAI/bge-small-en-v1.5 --batch-size 4 --port 8000 &
60+
```
61+
62+
Then use a load balancer (nginx, HAProxy, etc.) to distribute requests across
63+
ports. Throughput scales linearly with cores: 2 cores = 2x, 4 cores = 4x.
64+
65+
### 5. Test it
66+
67+
```bash
68+
curl http://localhost:7997/embeddings \
69+
-H "Content-Type: application/json" \
70+
-d '{"input": ["Hello world", "How are you?"], "model": "BAAI/bge-small-en-v1.5"}'
71+
```
72+
73+
## Performance (bge-small-en-v1.5, batch_size=4)
74+
75+
### Latency (serial requests, P50)
76+
77+
| Workload | g5.xlarge (GPU) | inf2.xlarge (1 core) | trn2.3xlarge (1 core) |
78+
|----------|----------------|---------------------|----------------------|
79+
| 1 short sentence | 14.2ms | 25.0ms | 19.0ms |
80+
| 4 short sentences | 16.0ms | 25.6ms | 19.5ms |
81+
| 4 long sentences | 16.2ms | 26.0ms | 20.3ms |
82+
83+
### Throughput (concurrent requests, data parallelism)
84+
85+
| Instance | Cores | Peak emb/s | Concurrency |
86+
|----------|-------|-----------|-------------|
87+
| g5.xlarge (GPU) | 1 GPU | 536 | 8 |
88+
| inf2.xlarge | 1 core | 216 | 4 |
89+
| inf2.xlarge | 2 cores | 427 | 4 |
90+
| trn2.3xlarge | 1 core | 348 | 4 |
91+
| trn2.3xlarge | 4 cores | 753 | 4 |
92+
93+
**Notes:**
94+
- g5.xlarge uses `--engine torch`; inf2/trn2 use `--engine neuron`
95+
- Neuron latency is constant regardless of batch content (padded to compiled batch size)
96+
- trn2 has ~30% lower latency per core than inf2 (19ms vs 25ms)
97+
- Throughput scales linearly with data parallelism (1 process per core)
98+
- Compilation time: ~60-100 seconds on first run (cached after that)
99+
100+
Tested on HuggingFace Neuron AMI (optimum-neuron 0.4.4, neuronx-cc 2.21, SDK 2.27)
101+
and Deep Learning AMI Neuron Ubuntu 22.04 (SDK 2.28) for trn2.
102+
103+
## Tested Stack
104+
105+
| Package | Version |
106+
|---------|---------|
107+
| optimum-neuron | 0.4.4 |
108+
| optimum | 2.0.0 |
109+
| neuronx-cc | 2.21.33363 |
110+
| torch-neuronx | 2.8.0.2.10 |
111+
| torch | 2.8.0 |
112+
| transformers | 4.57.3 |
113+
| Python | 3.10.12 |
114+
115+
## Alternative: Docker
116+
117+
### Build from source
10118

11-
### Optional: build docker image from scratch
12119
```bash
13120
git clone https://github.com/michaelfeil/infinity
14121
cd infinity
15-
docker buildx build -t michaelf34/infinity:0.0.x-neuron -f ./infra/aws_neuron/Dockerfile.neuron
122+
docker buildx build -t infinity-neuron -f ./infra/aws_neuron/Dockerfile.neuron .
16123
```
17124

18-
### Run the image on EC2
125+
### Run on EC2
19126

20127
```bash
21-
docker run -it --rm --device=/dev/neuron0 michaelf34/infinity:0.0.71-neuron v2 --model-id BAAI/bge-small-en-v1.5 --batch-size 8 --log-level debug
128+
docker run -it --rm --device=/dev/neuron0 infinity-neuron \
129+
v2 --model-id BAAI/bge-small-en-v1.5 --batch-size 8
22130
```
23131

24-
### Run task on ECS (Work in progress)
132+
**Note:** The host must have the Neuron driver installed. The Docker approach is less tested than the direct AMI approach above.
133+
134+
## Limitations
25135

26-
1. Create a AWS ECS Cluster with EC2:
27-
- Amazon Machine Image (AMI): Amazon Linux 2 - *Neuron*
28-
- inf2.xlarge as machine type.
136+
- The `--engine neuron` flag currently supports **text embeddings only** (no reranking or classification)
137+
- The Neuron engine requires a **constant batch size** (requests are padded automatically)
138+
- Models are compiled on first use; compilation can take 60-120 seconds
139+
140+
## ECS Deployment
141+
142+
See the ECS task definition example below for container orchestration:
29143

30-
2. Create a Task:
31144
```json
32145
{
33146
"family": "ecs-infinity-neuron",
@@ -45,10 +158,7 @@ docker run -it --rm --device=/dev/neuron0 michaelf34/infinity:0.0.71-neuron v2 -
45158
"executionRoleArn": "${YOUR_EXECUTION_ROLE}",
46159
"containerDefinitions": [
47160
{
48-
"entryPoint": [
49-
"infinity_emb",
50-
"v2"
51-
],
161+
"entryPoint": ["infinity_emb", "v2"],
52162
"portMappings": [
53163
{
54164
"hostPort": 7997,
@@ -61,41 +171,19 @@ docker run -it --rm --device=/dev/neuron0 michaelf34/infinity:0.0.71-neuron v2 -
61171
{
62172
"containerPath": "/dev/neuron0",
63173
"hostPath": "/dev/neuron0",
64-
"permissions": [
65-
"read",
66-
"write"
67-
]
174+
"permissions": ["read", "write"]
68175
}
69176
],
70177
"capabilities": {
71-
"add": [
72-
"IPC_LOCK"
73-
]
178+
"add": ["IPC_LOCK"]
74179
}
75180
},
76181
"cpu": 0,
77182
"memoryReservation": 1000,
78-
"image": "michaelf34/infinity:0.0.71-neuron",
183+
"image": "infinity-neuron:latest",
79184
"essential": true,
80185
"name": "infinity-neuron"
81186
}
82187
]
83188
}
84189
```
85-
86-
You can also add logging:
87-
```
88-
// same indent as "linuxParameters"
89-
"logConfiguration": {
90-
"logDriver": "awslogs",
91-
"options": {
92-
"awslogs-group": "/ecs/ecs-infinity-neuron",
93-
"mode": "non-blocking",
94-
"awslogs-create-group": "true",
95-
"max-buffer-size": "25m",
96-
"awslogs-region": "us-west-2", // set correct location.
97-
"awslogs-stream-prefix": "ecs"
98-
},
99-
"secretOptions": []
100-
}
101-
```

libs/infinity_emb/infinity_emb/transformer/acceleration.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@
88
from infinity_emb.primitives import Device
99

1010
if CHECK_OPTIMUM.is_available:
11-
from optimum.bettertransformer import ( # type: ignore[import-untyped]
12-
BetterTransformer,
13-
BetterTransformerManager,
14-
)
11+
try:
12+
from optimum.bettertransformer import ( # type: ignore[import-untyped]
13+
BetterTransformer,
14+
BetterTransformerManager,
15+
)
16+
except (ImportError, ModuleNotFoundError):
17+
# optimum.bettertransformer was removed in optimum >= 2.0
18+
CHECK_OPTIMUM.mark_dirty(
19+
ImportError(
20+
"optimum.bettertransformer is not available in this version of optimum. "
21+
"BetterTransformer support requires optimum < 2.0."
22+
)
23+
)
1524

1625
if CHECK_TORCH.is_available:
1726
import torch
@@ -37,6 +46,9 @@ def check_if_bettertransformer_possible(engine_args: "EngineArgs") -> bool:
3746
if not engine_args.bettertransformer:
3847
return False
3948

49+
if "BetterTransformerManager" not in globals():
50+
return False
51+
4052
config = AutoConfig.from_pretrained(
4153
pretrained_model_name_or_path=engine_args.model_name_or_path,
4254
revision=engine_args.revision,

libs/infinity_emb/infinity_emb/transformer/embedder/neuron.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
# Copyright (c) 2023-now michaelfeil
33

44
import copy
5-
import json
6-
import subprocess
7-
from typing import Union
8-
from functools import cache
5+
import os
96
import numpy as np
107

118
from infinity_emb._optional_imports import CHECK_OPTIMUM_NEURON, CHECK_TORCH
@@ -30,22 +27,6 @@
3027
]
3128

3229

33-
@cache
34-
def get_nc_count() -> Union[int, None]:
35-
"""Returns the number of neuron cores on the current instance."""
36-
try:
37-
cmd = "neuron-ls --json-output"
38-
result = subprocess.run(cmd, shell=True, capture_output=True)
39-
print("inferring nc_count from `neuron-ls`")
40-
print(result.stdout.decode("utf-8"))
41-
json_output = json.loads(result.stdout)
42-
count = sum([x["nc_count"] for x in json_output])
43-
print(f"nc_count={count}")
44-
return count
45-
except Exception:
46-
return None
47-
48-
4930
def pad_up_to_size(desired_max_bs: int, input_ids: "torch.Tensor") -> "torch.Tensor":
5031
"""input_ids a 2D array with batch_size on dim=0
5132
@@ -97,7 +78,13 @@ def __init__(self, *, engine_args: EngineArgs):
9778
)
9879
self._infinity_tokenizer = copy.deepcopy(self.tokenizer)
9980

100-
compiler_args = {"num_cores": get_nc_count(), "auto_cast_type": "fp16"}
81+
# Default to 1 NeuronCore (data parallelism). For large models that
82+
# require tensor parallelism across multiple cores, set the
83+
# NEURON_NUM_CORES environment variable. For data-parallel scaling,
84+
# run separate server processes pinned to individual cores via
85+
# NEURON_RT_VISIBLE_CORES (see infra/aws_neuron/README.md).
86+
num_cores = int(os.environ.get("NEURON_NUM_CORES", "1"))
87+
compiler_args = {"num_cores": num_cores, "auto_cast_type": "fp16"}
10188
input_shapes = {
10289
"batch_size": engine_args.batch_size,
10390
"sequence_length": (
@@ -124,7 +111,6 @@ def encode_pre(self, sentences: list[str]) -> dict[str, "torch.Tensor"]:
124111
padding=True,
125112
truncation="longest_first",
126113
return_tensors="pt",
127-
return_token_type_ids=False,
128114
)
129115
return input_dict
130116

0 commit comments

Comments
 (0)