feat: 边缘侧服务代码初始化与配置同步准备

This commit is contained in:
2026-05-08 11:33:43 +08:00
parent ee9be85292
commit 2e7245d62e
9 changed files with 526 additions and 0 deletions

149
bench_npu.py Normal file
View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
昇腾 310B4 NPU 推理性能基准测试
测试项目:
1. 确认 NPU 推理(非 CPU fallback
2. 单次推理延迟
3. 连续推理吞吐 (FPS)
4. 多 worker 并发
5. 不同分辨率影响
"""
import os
import sys
import time
import socket
import struct
import json
import base64
import threading
import numpy as np
import cv2
from concurrent.futures import ThreadPoolExecutor
SOCK_PATH = "/tmp/edge-infer.sock"
def make_test_image(w=640, h=640):
"""生成随机测试图片"""
img = np.random.randint(0, 255, (h, w, 3), dtype=np.uint8)
_, buf = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 90])
return buf.tobytes()
def infer_one(jpeg_bytes):
"""发送一次推理请求,返回响应时间(ms)"""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(SOCK_PATH)
msg = {
"stream_id": 0,
"device_id": "bench",
"url": "bench://test",
"ts": 1234567890.0,
"jpeg_b64": base64.b64encode(jpeg_bytes).decode("utf-8")
}
data = json.dumps(msg).encode("utf-8")
t0 = time.time()
sock.sendall(struct.pack(">I", len(data)) + data)
hdr = sock.recv(4)
length = struct.unpack(">I", hdr)[0]
result = json.loads(sock.recv(length).decode("utf-8"))
elapsed = (time.time() - t0) * 1000 # ms
sock.close()
return elapsed, result
def test_single_infer():
"""单次推理延迟"""
print("\n=== 单次推理延迟测试 ===")
jpeg = make_test_image()
times = []
for i in range(5):
t, _ = infer_one(jpeg)
times.append(t)
print(f"{i+1} 次: {t:.1f} ms")
# 跳过第一次预热
times = times[1:]
avg = sum(times) / len(times)
print(f"\n 平均延迟 (排除预热): {avg:.1f} ms")
print(f" 理论 FPS: {1000/avg:.1f}")
return avg
def test_throughput():
"""连续推理吞吐"""
print("\n=== 连续推理吞吐测试 ===")
jpeg = make_test_image()
count = 30
t0 = time.time()
for i in range(count):
_, _ = infer_one(jpeg)
if (i+1) % 10 == 0:
elapsed = time.time() - t0
print(f" {i+1}/{count} 完成, 累计: {elapsed:.1f}s, FPS: {(i+1)/elapsed:.1f}")
total = time.time() - t0
fps = count / total
print(f"\n 总计 {count} 帧: {total:.2f}s")
print(f" 吞吐: {fps:.1f} FPS")
print(f" 每帧延迟: {total/count*1000:.1f} ms")
return fps
def test_concurrent(workers=4):
"""多 worker 并发推理"""
print(f"\n=== {workers} 路并发测试 ===")
jpeg = make_test_image()
results_per_worker = []
def worker_task(worker_id, n_frames=20):
times = []
for _ in range(n_frames):
t, _ = infer_one(jpeg)
times.append(t)
return worker_id, times
t0 = time.time()
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(worker_task, i, 15) for i in range(workers)]
for f in futures:
wid, times = f.result()
results_per_worker.append((wid, sum(times)/len(times), max(times), min(times)))
total = time.time() - t0
total_frames = workers * 15
total_fps = total_frames / total
for wid, avg, mx, mn in results_per_worker:
print(f" Worker {wid}: avg={avg:.1f}ms, max={mx:.1f}ms, min={mn:.1f}ms")
print(f"\n 并发 {workers} 路: 总计 {total_frames} 帧, {total:.2f}s")
print(f" 总吞吐: {total_fps:.1f} FPS")
print(f" 单路等效 FPS: {total_fps/workers:.1f}")
return total_fps
def test_power():
"""读取 NPU 功耗信息"""
import subprocess
try:
result = subprocess.run(['npu-smi', 'info'], capture_output=True, text=True, timeout=5)
print("\n=== NPU 状态 ===")
print(result.stdout)
except Exception as e:
print(f"无法读取 NPU 状态: {e}")
if __name__ == "__main__":
print("=" * 60)
print(" 昇腾 310B4 + ACL 原生推理 性能基准测试")
print("=" * 60)
test_power()
test_single_infer()
test_throughput()
test_concurrent(2)
test_concurrent(4)
test_concurrent(6)
print("\n" + "=" * 60)
print("测试完成")
print("=" * 60)

13
config/edge.yaml Normal file
View File

@@ -0,0 +1,13 @@
edge_id: edge-demo-001
cloud_url: http://101.36.73.102:8004
edge_token: ""
# RTSP 流地址(留空使用演示模式)
rtsp_urls: []
infer_socket: /tmp/edge-infer.sock
infer_fps: 5
infer_workers: 3
conf_threshold: 0.5
dedup_window_sec: 30
version: 1.0.0

119
debug_model.py Normal file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""调试: 检查模型原始输出"""
import numpy as np
import cv2
import acl
ACL_MEMCPY_HOST_TO_DEVICE = 1
ACL_MEMCPY_DEVICE_TO_HOST = 2
img = cv2.imread("/home/强光车灯误报.png")
print("Image shape:", img.shape)
acl.init()
acl.rt.set_device(0)
ctx, _ = acl.rt.create_context(0)
model_id, _ = acl.mdl.load_from_file("/root/AI-tianyan/model/model.om")
desc = acl.mdl.create_desc()
acl.mdl.get_desc(desc, model_id)
in_sz = acl.mdl.get_input_size_by_index(desc, 0)
print("Input size:", in_sz, "bytes")
# Preprocess
h0, w0 = img.shape[:2]
inp_h, inp_w = 640, 640
scale = min(inp_h / h0, inp_w / w0)
nh, nw = int(h0 * scale), int(w0 * scale)
resized = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
canvas = np.full((inp_h, inp_w, 3), 114, dtype=np.uint8)
pad_top = (inp_h - nh) // 2
pad_left = (inp_w - nw) // 2
canvas[pad_top:pad_top + nh, pad_left:pad_left + nw] = resized
rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
blob = rgb.astype(np.float32) / 255.0
blob = np.ascontiguousarray(blob.transpose(2, 0, 1)[np.newaxis])
print("Blob shape:", blob.shape, "dtype:", blob.dtype)
# Allocate device input
in_ds = acl.mdl.create_dataset()
host_buf = np.ascontiguousarray(blob)
host_addr = acl.util.bytes_to_ptr(host_buf.tobytes())
in_buf, _ = acl.rt.malloc(in_sz, 0)
acl.rt.memcpy(in_buf, in_sz, host_addr, in_sz, ACL_MEMCPY_HOST_TO_DEVICE)
acl.mdl.add_dataset_buffer(in_ds, acl.create_data_buffer(in_buf, in_sz))
# Allocate output
out_num = acl.mdl.get_num_outputs(desc)
out_sizes = [acl.mdl.get_output_size_by_index(desc, i) for i in range(out_num)]
out_ds = acl.mdl.create_dataset()
out_bufs = []
for i in range(out_num):
buf, _ = acl.rt.malloc(out_sizes[i], 0)
out_bufs.append(buf)
acl.mdl.add_dataset_buffer(out_ds, acl.create_data_buffer(buf, out_sizes[i]))
print(f"Output[{i}] size: {out_sizes[i]} bytes")
# Execute
ret = acl.mdl.execute(model_id, in_ds, out_ds)
print("Execute ret:", ret)
# Get output
for i in range(out_num):
sz = out_sizes[i]
host = np.zeros(sz, dtype=np.uint8)
host_addr = acl.util.bytes_to_ptr(host.tobytes())
acl.rt.memcpy(host_addr, sz, out_bufs[i], sz, ACL_MEMCPY_DEVICE_TO_HOST)
dims_out, _ = acl.mdl.get_output_dims(desc, i)
d = dims_out['dims']
print(f"\nOutput[{i}] dims: {d}")
print(f" Size: {sz} bytes")
# Try FP16
fp16 = host.view(np.float16)
print(f" FP16 shape: {fp16.shape}")
reshaped = fp16.astype(np.float32).reshape(d)
print(f" Reshaped: {reshaped.shape}")
print(f" Min: {reshaped.min():.4f}, Max: {reshaped.max():.4f}, Mean: {reshaped.mean():.4f}")
# Check if it's all zeros or NaN
nan_count = np.isnan(reshaped).sum()
zero_count = (reshaped == 0).sum()
print(f" NaN count: {nan_count}, Zero count: {zero_count}/{reshaped.size}")
# For YOLOv8 output [1, 84, 8400], check class scores
# boxes: reshaped[:, :4, :]
# scores: reshaped[:, 4:, :]
scores = reshaped[0, 4:, :]
max_scores = scores.max(axis=0) # max class score per anchor
print(f"\n Max class scores per anchor:")
print(f" Min: {max_scores.min():.4f}, Max: {max_scores.max():.4f}, Mean: {max_scores.mean():.4f}")
# Count anchors with score > 0.1
high_conf = (max_scores > 0.1).sum()
print(f" Anchors with conf > 0.1: {high_conf}")
print(f" Anchors with conf > 0.01: {(max_scores > 0.01).sum()}")
# Print top 5
top5_idx = np.argsort(max_scores)[-5:][::-1]
for idx in top5_idx:
best_cls = scores[:, idx].argmax()
print(f" Anchor {idx}: class={best_cls} (score={max_scores[idx]:.4f})")
box = reshaped[0, :4, idx]
print(f" box: {box}")
# Cleanup
for buf in out_bufs:
acl.rt.free(buf)
acl.rt.free(in_buf)
acl.mdl.destroy_dataset(in_ds)
acl.mdl.destroy_dataset(out_ds)
acl.mdl.unload(model_id)
acl.mdl.destroy_desc(desc)
acl.rt.destroy_context(ctx)
acl.rt.reset_device(0)
acl.finalize()
print("\nDone")

76
detect_one.py Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""检测单张图片 - 通过 Unix socket 发送到推理服务"""
import sys
import socket
import struct
import json
import base64
import cv2
SOCK_PATH = "/tmp/edge-infer.sock"
def detect_image(image_path):
img = cv2.imread(image_path)
if img is None:
print("无法读取图片:", image_path)
return
# 编码为 JPEG
_, buf = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 90])
jpeg_bytes = buf.tobytes()
# 连接推理服务
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(SOCK_PATH)
# 构造请求
msg = {
"stream_id": 0,
"device_id": "cli-test",
"url": "file://" + image_path,
"ts": 1234567890.0,
"jpeg_b64": base64.b64encode(jpeg_bytes).decode("utf-8")
}
# 发送
data = json.dumps(msg).encode("utf-8")
sock.sendall(struct.pack(">I", len(data)) + data)
# 接收结果
hdr = sock.recv(4)
if not hdr:
print("未收到响应")
sock.close()
return
length = struct.unpack(">I", hdr)[0]
result = json.loads(sock.recv(length).decode("utf-8"))
sock.close()
# 输出结果
dets = result.get("detections", [])
print(f"\n图片: {image_path}")
print(f"尺寸: {img.shape[1]}x{img.shape[0]}")
print(f"检测到 {len(dets)} 个目标\n")
print(f"{'类别':<20} {'置信度':<10} {'边界框'}")
print("-" * 60)
for d in dets:
bbox = d["bbox"]
print(f'{d["class"]:<20} {d["conf"]:<10.3f} [{bbox[0]:.0f}, {bbox[1]:.0f}, {bbox[2]:.0f}, {bbox[3]:.0f}]')
# 保存带标注的图片
if dets:
for d in dets:
x1, y1, x2, y2 = [int(x) for x in d["bbox"]]
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
label = f'{d["class"]} {d["conf"]:.2f}'
cv2.putText(img, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
out_path = image_path.rsplit(".", 1)[0] + "_result.jpg"
cv2.imwrite(out_path, img)
print(f"\n已保存标注图片: {out_path}")
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "/home/强光车灯误报.png"
detect_image(path)

10
go.sum Normal file
View File

@@ -0,0 +1,10 @@
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

BIN
model/model.om Normal file

Binary file not shown.

80
model/names.txt Normal file
View File

@@ -0,0 +1,80 @@
person
bicycle
car
motorcycle
airplane
bus
train
truck
boat
traffic light
fire hydrant
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
backpack
umbrella
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
couch
potted plant
bed
dining table
toilet
tv
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
book
clock
vase
scissors
teddy bear
hair drier
toothbrush

79
test_infer.py Normal file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""测试推理服务 - 发送一张测试图片"""
import socket
import struct
import json
import base64
import numpy as np
import cv2
import os
SOCK_PATH = "/tmp/edge-infer.sock"
def create_test_image():
"""创建一张 640x480 的测试图片,画几个几何图形"""
img = np.zeros((480, 640, 3), dtype=np.uint8)
# 画一些简单的形状模拟检测目标
cv2.rectangle(img, (50, 50), (200, 200), (255, 255, 255), -1)
cv2.circle(img, (400, 300), 80, (255, 255, 255), -1)
# 编码为 JPEG
_, buf = cv2.imencode('.jpg', img)
return buf.tobytes()
def send_msg(conn, payload):
data = json.dumps(payload).encode("utf-8")
conn.sendall(struct.pack(">I", len(data)) + data)
def recv_msg(conn):
hdr = conn.recv(4)
if not hdr:
return None
length = struct.unpack(">I", hdr)[0]
data = b""
while len(data) < length:
chunk = conn.recv(length - len(data))
if not chunk:
return None
data += chunk
return json.loads(data.decode("utf-8"))
def main():
if not os.path.exists(SOCK_PATH):
print(f"错误: socket {SOCK_PATH} 不存在")
return
print("连接推理服务...")
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(SOCK_PATH)
# 创建测试图片
jpeg_bytes = create_test_image()
msg = {
"stream_id": 0,
"device_id": "test-001",
"url": "test://local",
"ts": 1234567890.0,
"jpeg_b64": base64.b64encode(jpeg_bytes).decode("utf-8")
}
print("发送测试图片...")
send_msg(conn, msg)
print("等待推理结果...")
result = recv_msg(conn)
if result:
print(f"\n推理结果:")
print(f" stream_id: {result['stream_id']}")
print(f" device_id: {result['device_id']}")
print(f" detections: {len(result['detections'])} 个目标")
for d in result['detections']:
print(f" - {d['class']}: {d['conf']:.3f} bbox={d['bbox']}")
else:
print("未收到结果")
conn.close()
print("\n测试完成!")
if __name__ == "__main__":
main()

BIN
yolov8n.om Normal file

Binary file not shown.