news 2026/9/19 5:06:33

UE5集成Entt ECS:大规模实体性能优化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UE5集成Entt ECS:大规模实体性能优化实践

我最早动这个念头,是因为项目里一个玩法模块的实体数量冲到了8000以上。UE5原生Actor加Component的组合在编辑器里跑得很欢,一到打包版本就开始肉眼可见地掉帧,Profiler一拉,逻辑线程的Tick开销和数据碎片全堆在那里。后来把Entt接进去,才真正体会到ECS在纯逻辑层能省下多少成本。这篇文章就是把我从按头硬啃到落地跑通的完整过程捋一遍,说说为什么用Entt而不是DataAsset加结构体硬撸,Registry生命周期怎么和UE5的世界管理协调,以及那些文档里不会告诉你的坑。

1. UE5原生的组件体系,瓶颈究竟在哪里

1.1 Actor和UActorComponent在批量场景下的真实开销

UE5的Actor体系本质上是一个树形对象模型。每个Actor有一个Transform、一个RootComponent,再挂上若干个UActorComponent。这个结构贴合编辑器操作,做单物体逻辑也确实顺手,但它有两个天生的问题:一是UObject本身的反射、GC、序列化信息是实打实的内存开销,二是组件访问路径太长。

举个例子,你要在游戏里做一万个漂浮的小光点,每个需要每帧读取自己的位置和速度,做一次积分,再写回位置。用Actor加UStaticMeshComponent或者UPrimitiveComponent来做,你的访问路径是:

拿到Actor指针,查组件数组,找到目标组件,确认类型转换,再通过GetComponentLocation这类带虚函数性质的接口去拿数据。一套下来,中间层层判断,缓存早就跳飞了。

这不是代码写得不好的问题,是对象模型本身决定了你没法把数据紧凑地排在一起。一万个Actor,光存储开销就非常可观,更不用说遍历时Cache Miss带来的性能灾难。我用Unreal Insights拉过一次数据,逻辑线程里这种简单的更新逻辑,光缓存未命中的耗时占比就超过了50%。

1.2 为什么数据驱动在这里是解药

ECS的核心思路是:实体只是个整数ID,组件是纯数据结构,系统是处理这些数据的函数。真正让ECS性能强的原因是内存布局——它把所有同类型组件连续存放在一起,遍历时按顺序读取,最大化缓存命中率。

如果把上面那个漂浮光点的例子改用ECS实现,位置、速度、生命值全部是紧密排列的数组,系统遍历时就是一个纯线性的for循环,编译器还能做向量化。这个差异在只有几十个实体时看不出来,一旦超过几千,差别是一个数量级的。

但这不代表ECS能解决所有问题。UE5的地形、物理、网络同步依然依赖引擎原生对象。ECS适合的是那些大量、同构、逻辑简单的实体群体,敌人波次、子弹系统、粒子式的道具刷新、AI决策状态机。理解这个边界之后,再用Entt才算用对了地方。

2. Entt的核心机制,和UE5常见的理解错位

2.1 Registry、Entity、Component三个基本概念

Entt整个框架最核心的东西就是entt::registry。它是一个管理所有实体和组件存储的容器,也是唯一一个你需要长期持有的对象。

  • entt::entity:本质上是一个ID,通常是32位整数,高位用来做版本号,低位是实际索引。实体本身不存任何数据。
  • entt::registry::create():创建一个新实体,返回实体ID。
  • entt::registry::emplace<T>():给实体挂上类型为T的组件数据。
  • entt::registry::view<T>():获取所有拥有T组件的实体的视图。

在UE5里找对应关系的话,registry有点像UWorld管理整个场景里的Actor和Component,entity就像Actor引用,emplace类似于AddComponentByClass。区别在于,UE5的Component是UObject,有生命周期和GC,而Entt的组件就是裸数据,你要自己决定它什么时候生、什么时候死。

2.2 view查询和group打包怎么选

当你有多种组件时,view<T, U>()是最常用的查询方式。它背后的实现是对多个组件存储做交集运算,选定一个遍历主容器,然后判断其它容器中是否存在该实体。这个方案的好处是灵活,运行时可以动态增删组件类型,坏处是当匹配的实体数量很少时,遍历浪费比较大。

group则不同,它接受一个类型列表,可以在插入组件时就维护一个排序或过滤后的列表结构。当你有大量查询同一个组合的需求、且该组合在运行期不会频繁变动时,group性能远高于view。

我个人在项目里的做法是:默认用view,如果Profiler显示某个系统成为了热点,而且查询的组合稳定,再改成group。不建议一开始就全面使用group,因为其构建和维护成本也不是零,需要看实际场景。

2.3 dispatcher事件分发和UE5的事件系统并不冲突

Entt还带了一个基于信号槽的dispatcher,用于系统间通信。它和UE5的GameplayMessageSubsystemBlueprintNativeEvent这类机制功能上有重叠,但理念不同。Entt的dispatcher是纯C++的,不涉及UObject反射,广播速度比UE5的事件快很多。

但我不建议把所有UE5事件都改成dispatcher。UE5的GameplayMessage更适合蓝图侧的消息通信,dispatcher更适合纯逻辑层内部的系统解耦。举个例子,战斗逻辑里"攻击命中"事件用Dispatcher广播给受伤表现、音效、飘字,比用UE5的Event解耦更彻底,也不会有GC引用问题。

3. 把Entt请进UE5:完整集成落地方案

3.1 获取Entt头文件并加入工程

Entt是header-only库,核心代码全部在头文件里,不需要编译Lib。官方仓库在GitHub上,直接找最新的release tag,把src/entt目录拷贝到你的项目Source下的ThirdParty目录里即可。

在UE5里有两种组织方式。一种是整个工程级别的ThirdParty文件夹,另一种是放到某个模块下。我建议放模块级ThirdParty/Entt目录,方便后续按模块独立引用。目录结构大概是这样:

Source/ MyGameModule/ ThirdParty/ Entt/ entt/ entity/ core/ process/ ...

3.2 修改Build.cs和模块依赖

拿到头文件之后,需要让编译期能找到Entt的include路径。在模块的Build.cs里添加:

PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "ThirdParty/Entt")); PublicDefinitions.Add("ENTT_PACKED_PAGE=128");

ENTT_PACKED_PAGE这个宏可以调整组件存储的页大小,默认128字节,如果组件很小可以调小减少内存碎片。这个不是必须,但值得关注。

另外还要检查你的模块依赖里有没有CoreEngineUEnttWorldSubsystem这类对象需要Engine模块支持。所以在PublicDependencyModuleNames里确保有这两个。

还要注意一个编码相关的坑:Entt在Windows上用MSVC编译时,如果开启/W4会有大量警告。建议在Build.cs里把该模块的WarningLevel调低,或者加bEnableUndefinedIdentifierWarnings = false,否则日志刷屏会导致你忽略真正重要的警告。

3.3 用UWorldSubsystem管理Registry生命周期

Registry需要在World存在期间一直有效,最合适宿主是UWorldSubsystem。它随World创建而创建,随World销毁而销毁,天然契合ECS生命周期的需求。

示例代码:

#pragma once #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "Entt/Entt.h" #include "MyEnttWorldSubsystem.generated.h" UCLASS() class MYGAMEMODULE_API UMyEnttWorldSubsystem : public UWorldSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; virtual bool ShouldCreateSubsystem(UObject* Outer) const override; virtual void Tick(float DeltaTime) override; virtual TStatId GetStatId() const override; entt::registry& GetRegistry() { return Registry; } const entt::registry& GetRegistry() const { return Registry; } private: entt::registry Registry; FTSTicker::FDelegateHandle TickHandle; };

在Initialize里注册每帧Tick,在Deinitialize里把Registry清掉。

void UMyEnttWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); TickHandle = FTSTicker::GetCoreTicker().AddTicker( FTickerDelegate::CreateUObject(this, &UMyEnttWorldSubsystem::TickCallback), 0.0f ); } void UMyEnttWorldSubsystem::Deinitialize() { if (TickHandle.IsValid()) { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); TickHandle.Reset(); } Registry.clear(); Super::Deinitialize(); }

原则上系统逻辑不该在这里直接写,而是单独拆一层System/Manger。但最基础的驱动点就靠这个每帧回调。

3.4 第一个可运行的ECS示例

下面写一个最小的循环:创建N个实体,每帧更新它们的位置,再把结果同步给对应Actor。

// 组件定义,纯数据结构 struct FPosition { FVector Value; }; struct FVelocity { FVector Value; }; void UMyEnttWorldSubsystem::Tick(float DeltaTime) { auto view = Registry.view<FPosition, FVelocity>(); view.each([DeltaTime](auto& Pos, auto& Vel) { Pos.Value += Vel.Value * DeltaTime; }); }

这个每帧遍历一万个组件的耗时比遍历Actor组件快接近一个数量级。但你很快会发现一个问题:光更新ECS数据没有意义,渲染和Gameplay还是需要Actor对象来显示。这就引出下一章的对象桥接问题。

4. UObject与Entt互操作,身份映射是最大难题

4.1 直接存UObject*进去?强烈不建议

新手第一反应是把Actor指针直接塞进组件结构体里:

struct FRenderLink { AActor* ActorRef; };

这会立刻踩中两个大坑。

第一,GC引用问题。UE5的UObject由GC管理,如果你只在Entt里持有裸指针,而UPROPERTY没有声明引用,GC可能随时把这个Actor回收掉,留下一张废纸指针。

第二,即使你侥幸没被回收,Actor被Destroy之后指针就是悬垂的。你需要在系统遍历时判断IsValid,否则一个野指针直接访问会让游戏崩溃。

正确思路是:Entt侧不持有UObject强引用,只保存ID或者TWeakObjectPtr。当真正需要访问Actor时再解引用并做有效性判断。

4.2 用EntityHandle映射UObject

我推荐的做法是为Actor和一个Entt实体建立双向映射。

在Actor侧,保存一个EnttEntity的ID:

USTRUCT(BlueprintType) struct FEnttEntityHandle { GENERATED_BODY() uint32 Index = 0; uint32 Version = 0; };

Entt的默认实现中,实体的索引和版本号可以从entt::entity中拆出来。这样Actor在需要数据时,通过世界Subsystem查Registry里关联的组件。

在Entt侧,组件里保存TWeakObjectPtr<AActor>

struct FActorLink { TWeakObjectPtr<AActor> Actor; };

这样两边都不形成强引用,GC不会因为Entt导致Actor无法回收,Actor销毁后WeakPtr自动变null。

在生成时建立映射:

entt::entity Entity = Registry.create(); Registry.emplace<FPosition>(Entity, SpawnLocation); Registry.emplace<FActorLink>(Entity, NewActor); if (auto* Link = Registry.try_get<FActorLink>(Entity)) { if (AActor* Actor = Link->Actor.Get()) { // 把EntityID存到Actor的自定义结构里 // 后续Actor再需要数据时可以通过这个ID反向查Registry } }

4.3 回调驱动的同步策略

纯ECS和UE5渲染层脱节,所以每次数据更新后,需要把必要的变换同步到Actor上。高频同步会很浪费,低频同步又会影响表现。项目里我用了脏标记的思路:只在组件数据变化时打标记,系统每帧只处理被标记的实体。

例如给每个实体加一个FDirtyFlag组件,位移有变化就置true,渲染同步系统只处理有脏标记的实体。

4.4 延迟销毁处理

另一个高频坑是:在系统遍历过程中,直接销毁实体或Actor会破坏迭代器的有效性。Entt的registry.destroy()在遍历过程中调用会引发未定义行为。

正确做法是攒一个待删除列表,在遍历结束后统一销毁:

std::vector<entt::entity> ToDestroy; view.each([&ToDestroy](entt::entity Entity, FHealth& Health) { if (Health.Value <= 0.0f) { ToDestroy.push_back(Entity); } }); for (auto Entity : ToDestroy) { // 先通知Actor侧销毁表现 Registry.destroy(Entity); }

这一点和UE5引擎里遍历Actor后删Actor的坑一模一样,但表现得更隐蔽,因为Entt没有内建的"挂起销毁"机制。

5. 多线程和性能实测数据

5.1 什么情况下值得上多线程

ECS的一大卖点是逻辑系统天然可并行,但不是所有系统都值得。只有满足以下条件的系统才考虑多线程:

  • 系统间没有读写依赖。
  • 系统内部没有动态增删实体和组件。
  • 系统不直接调用UObject接口。

例如子弹位置更新、AI打分、碰撞检测前处理,这些纯数学计算都适合并行。涉及网络同步、Actor生成销毁、Blueprint事件触发的系统,老老实实放游戏线程。

5.2 UE5的ParallelFor与Entt的结合

UE5内置的ParallelFor是游戏线程和任务图结合的并行原语。可以把view的遍历拆分成多个批次:

auto& Registry = GetWorld()->GetSubsystem<UMyEnttWorldSubsystem>()->GetRegistry(); auto view = Registry.view<FPosition, FVelocity>(); const int32 NumEntities = view.size_hint(); const int32 BatchSize = 256; const int32 NumBatches = FMath::Max(1, FMath::CeilToInt((float)NumEntities / BatchSize)); ParallelFor(NumBatches, [this, &view, NumEntities, BatchSize](int32 BatchIndex) { const int32 Start = BatchIndex * BatchSize; const int32 End = FMath::Min(Start + BatchSize, NumEntities); // 注意view的each本身没有显式索引,这里改用each按顺序手动管理次数 // 也可以将view拷贝为vector后按索引区间处理 }, EParallelForFlags::BackgroundPriority);

这里不建议直接对view.each做并行,因为each内部迭代器的实现没有为并行拆分提供稳定索引序列。更稳定做法是先把需要遍历的实体收集到数组,再按区间并行。

5.3 实测:单线程与并行对比

我在测试场景里放置12000个实体,每个实体的更新逻辑是读取位置和速度做积分并写回。测试环境是台式机i7-12700KF、32GB内存、UE5.3版本,逻辑线程单线程对比ParallelFor。

测试项平均逻辑耗时说明
原生Actor组件遍历约8.2ms不含渲染开销,仅逻辑Tick
Entt单线程view遍历约1.1ms纯数据遍历,无缓存逻辑
Entt多线程ParallelFor约0.35ms受任务调度开销限制,并没到线性加速
Entt多线程Batch+手动分区约0.28ms进一步减少任务竞争

数据不会骗人。原生方案和Entt单线程差了约7倍,ParallelFor进一步把耗时压到原生方案的3.5%左右。但注意测试里没有包含生成Actor和同步Transfrom的耗时,实际项目中纯逻辑节省的这部分,在渲染同步时可能会加回去一部分。这也是我强调按场景区分系统的原因。

5.4 并行过程中绝不能做的事

在ParallelFor的lambda里绝对不能做下面这些事:

  • 调用Registry.create()Registry.destroy()。Registry容器内部会做内存分配和版本更新,不是线程安全的。
  • 访问UObject的属性和方法。UObject大部分接口只能在GameThread执行。
  • 使用Registry.view<T>()获取新的view。视图本身是轻量级对象,但底层存储的遍历在并行时如果有其他线程在写,会有数据竞争。
  • 动态emplaceremove组件。这在多线程下会导致存储重排,迭代器失效。

常见安全写法是:并行阶段只读组件数据,或者只写预分配好的输出结构体。结构体内如果有TArray之类的动态容器,也建议只写通过索引预先Resize好的TArray。最后在游戏线程再做真正的实体增删和Actor同步。

6. 集成过程中遇到的坑和排查链路记录

6.1 组件类型标识与UE5反射系统的冲突

Entt内部用类型信息做哈希,区分不同组件类型。当你把两个结构体定义在不同的模块,或者结构体定义在匿名命名空间里,Entt拿到的类型哈希可能不同,导致同一个逻辑id在不同翻译单元里对应不上。

这个问题的现象是:一个模块里emplace<MyStruct>正常,另一个模块里view<MyStruct>查到的实体数量是0。排查链路:

  • 先检查两个模块是否都include了同一个头文件。如果结构体在A模块内部定义,B模块直接复制了一份内容相同的定义,那在编译期它们就是两种不同类型。
  • 再检查首行是否static_assert(std::is_trivially_copyable<MyStruct>::value)能在两个模块都通过。Entt强依赖对象的可拷贝性。
  • 最终方案是把共享组件定义放到独立的Runtime公共模块,或者放到同一个Header里,并确保该Header被所有使用模块以相同路径include。

为了彻底杜绝这类问题,我给项目里所有组件定义加上统一的宏,放在公共模块的CommonComponents.h里,禁止在模块私有头里定义跨系统使用的组件。

6.2 Entity版本号溢出

Entt的entt::entity本质是个整型ID,高位是版本号。默认实现下版本号占一定位数,如果你的游戏逻辑中实体的创建和销毁极其频繁,比如一秒钟几千个子弹,长时间运行后版本号可能溢出回绕。

回绕的后果很严重:一个已经销毁的实体索引,在回绕后可能复用一个相同的高位版本号,导致一个"新"实体被误认为旧实体,之前持有的缓存指针失效。

排查中真正让我意识到这个概念的是在Profiler里发现偶发的数据错乱,但逻辑检查没有发现Bug。最后的手段是限制一个World生命周期内创建实体的总次数,如果超过std::numeric_limits<entt::id_type>::max() >> entt::entt_traits<entt::entity>::version_mask,就主动报错并触发World切换。

这算是在UE5环境里跑Entt时比较隐蔽但必须考虑的问题。

6.3 默认构造和值初始化差异

Entt的emplace有两种常见调用方式:

// 使用组件的默认构造结果 Registry.emplace<FMyComponent>(Entity); // 传入构造参数 Registry.emplace<FMyComponent>(Entity, InitValue);

如果组件结构体没有定义构造函数,而是通过UPROPERTY配合UObject风格初始化,你要小心。Entt内部会先调用placement new来构造对象,如果结构体里还有TArray或TMap这类UE容器,它们在原生C++下的构造和UE的构造行为一致,但拷贝语义需要额外确认。

比如组件结构体里有一个TArray<AActor*> WeakRefs,直接放进Entt后,如果调用Registry.replace<FMyComponent>(Entity, NewData),NewData里的指针数组会被浅拷贝,这和UE容器预期的深层语义不同,必须自己实现拷贝赋值运算符。

我用这个场景排查过一遍:批量替换组件数据后,发现部分Actor引用无效,检查发现是TArray浅拷贝导致老指针失效。最终方案是组件统一定义为POD类型,或者包含UE容器时全部用TWeakObjectPtr数组并且重载拷贝构造。

6.4 与UE5网络复制的兼容性

如果你的游戏有联机需求,直接在Entt Registry里维护的组件数据是不同步的。UE5的网络同步是基于Actor和Property的,纯C++的ECS不参与其中。

项目里的妥协方案是:网络端Actor始终存在,Entt只作为服务端的逻辑加速层。客户端需要同步的数据,由服务端通过UE5的ActorReplication机制发给客户端,客户端再反写回自己的Entt组件。这样既不牺牲服务端的逻辑性能,又不破坏引擎原有的网络模型。

加了这一层之后,性能提升虽然略打折扣,但换来的是蓝图层和网络层可以完全复用。

6.5 清理Registry释放内存

registry.clear()只清空实体和组件,但内部存储占用的内存不会完全归还给操作系统。这在长时间运行的地图类游戏中会导致内存碎片增长。

我在项目里做的方法是:每次大关卡切换时,直接销毁旧Registry并新建一个。由于Registry本身在WorldSubsystem里,自定义一个ResetWithNewRegistry()方法,内部调用Registry = entt::registry()

registry.shrink_to_fit()可以主动压缩存储,但如果还频繁创建销毁实体,压缩反而增加开销。建议在实体数量稳定后或者关卡加载完成后调用一次。

7. 我对Entt集成进UE5的几点总结性体验

7.1 组件设计范式要从项目早期就定好

我见过很多项目在中期引入ECS,结果因为组件边界不清晰,导致系统查询越来越慢,最终退化成"用ECS实现了原来的对象树"。解决方案是,组件保持纯粹的数据结构,禁止在组件里写逻辑。逻辑全放系统函数里,用系统职责来管理对这些数据的读写。

后续如果要加新功能,先想清楚这个新功能是数据还是逻辑。数据放Entt,逻辑放独立系统,Actor只做表现和输入转发。坚持这个原则半年后,代码可维护性提升非常明显。

7.2 给蓝图留后门,但别让蓝图成为主路径

Entt是纯C++库,蓝图无法直接访问Registry和组件数据。如果项目里蓝图比重很大,建议预先封装一套纯C++的API给蓝图调用,比如让蓝图通过UFunction调用"创建实体并挂组件"、"修改某实体某组件字段"等接口。

但如果核心Gameplay逻辑全部暴露给蓝图,那ECS的性能优势会逐渐被跨语言调用的开销抵消。我做的取舍是:逻辑层80%在C++里,只把业务表现和UI事件抛给蓝图。

7.3 性能优化时先量化再动手

用Entt不等于性能无忧。我在测试中还遇到过:某系统拿到view之后,每实体内部做了太多的动态内存分配,导致耗时反而比Actor方案更高。用Unreal Insights和TRACE_CPUPROFILER_EVENT_SCOPE逐帧看每个系统耗时,找出真正的热点系统,再针对性地做布局优化。

如果看Profiler发现一个系统耗时不正常地高,先怀疑是不是遍历时触发了组件存储的扩容或者组件拷贝,再怀疑是不是查询范围过大,最后才考虑是否要用group替代view。优化顺序不能反。

7.4 最后一个小建议

如果你项目里已经稳定运行了大量原生Actor,别指望引入Entt能一步到位解决所有性能问题。最稳妥的路径是:先选一个逻辑独立、实体数量大的子系统(比如子弹、敌人群体AI、技能特效触发),把那一块单独切到ECS,做好Profiler前后对比,用数据说服团队继续推进。

我这边从子弹系统切入,实测性能提升立竿见影后,再逐步把AI决策、场景物交互等模块迁移过来。这个渐进式方案比推倒重来风险小得多,也方便在迁移过程中积累适合你们项目的ECS使用规范。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 5:06:33

27B大模型真能塞进M.2?RK3588+后摩LQ50端侧推理实录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 5:06:25

基于CST的毫米波雷达ADAS仿真:从回波到RD图全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 5:06:05

列车通信网络TCN架构解析:从MVB/WTB到以太网化演进

在检修库待过的人都懂一个画面&#xff1a;一列车晚上入库时还好好的&#xff0c;第二天早上出库前&#xff0c;司机台报“网络通信故障”&#xff0c;整列车瘫痪在库里。调度催、检修急&#xff0c;仪表一个一个查下来&#xff0c;最后往往就是一个终端电阻氧化或者屏蔽层接地…

作者头像 李华
网站建设 2026/9/19 5:06:05

学生编程助手选型指南:零安装、离线可用、不打断思考流

1. 学生选编程助手&#xff0c;不是挑“最火”的&#xff0c;而是找“不打断思考流”的我带过三届校内编程工作坊&#xff0c;也帮过二十多个不同专业的本科生调试课设代码。最常听到的抱怨不是“不会写”&#xff0c;而是“刚理清思路&#xff0c;就被弹窗、卡顿、登录框、续费…

作者头像 李华
网站建设 2026/9/19 5:05:40

数字后端LVS调试实战:从Innovus到GDS的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 5:05:32

Atlas 300V 24G推理卡上部署YOLO的完整技术指南

看到不少人在搜“atlas部署yolo”和“atlas 300v 24g 是运算加速卡吗”&#xff0c;正好这两件事我最近都完整折腾过一遍。Atlas这个系列名字在华为昇腾生态里指代了好几种硬件&#xff0c;容易被绕晕&#xff0c;而300V 24G这块卡又是很多做视频分析、边缘推理的团队会重点考虑…

作者头像 李华