news 2026/10/16 8:26:54

Linux内核memblock机制解析:从早期内存管理到伙伴系统交接

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核memblock机制解析:从早期内存管理到伙伴系统交接

1. memblock是什么,为什么早期内存管理离不开它

做内核启动相关调试的人,十有八九都见过这么一幕:板子刚上电没几秒,串口输出停在某个早期函数,连调度器影子都没有,这时候如果怀疑是内存分配出了问题,能用的分析工具就是memblock。我最早接触Linux内存初始化这个话题,就是帮同事排查一个诡异的问题:内核启动过程中某段数据被莫名改写,查了半天才发现是早期的临时内存释放逻辑不对,memblock 的 reserved 区域没有包含那块地址。从那以后,每次梳理启动流程我都会先看一遍memblock初始化的完整链路,今天就把这些内容系统整理一下。

1.1 伙伴系统出生前的“临时托管机构”

内存管理是Linux内核最核心的子系统之一,但很多人忽略了它有一个“早期版本”。等到start_kernel走到mm_init,伙伴系统(buddy system)才算正式接管物理内存,而在此之前,连kmalloc都不可用,更别说slab了。那这段时间内核要分配内存怎么办?答案就是memblock。

你可以把memblock理解成一个极简的“临时托管机构”。它的任务特别单纯:在伙伴系统上线之前,把物理内存分成“可用的”和“被占用的”两类,谁需要用内存(比如建立页表、加载 initrd),就向它申请一块,它记录一下“这块归你了”,防止后续有人重复申请。

为什么不用 bootmem?早期Linux确实用过 bootmem,但 bootmem 基于位图管理,内存碎片多、节点扩展差,后来被 memblock 取代。现在的内核,从 ARM、ARM64、RISC-V 到 x86,基本都统一走 memblock 这条路。搞清楚 memblock,等于把“内核怎么从零开始认识物理内存”这个问题的前半段串起来了。

1.2 两张清单:memory与reserved

memblock 的实现思路极其朴素:维护两个区域列表,一个叫memory,一个叫reserved。

memory记录系统当前可用的物理内存区域,注意是“当前”,因为某些区域可能随时被移出。reserved记录已经被内核或者其他实体占用的区域,这些区域在释放之前不允许被分配。两个列表里的区域都用struct memblock_region表示:

struct memblock_region { phys_addr_t base; phys_addr_t size; enum memblock_flags flags; int nid; };

base是起始物理地址,size是长度,flags标记区域属性(比如可移动内存),nid是 NUMA 节点号。再往上两层:

struct memblock_type { unsigned long cnt; unsigned long max; phys_addr_t total_size; struct memblock_region *regions; char *name; }; struct memblock { bool bottom_up; phys_addr_t current_limit; struct memblock_type memory; struct memblock_type reserved; };

关键的current_limit后面会单独讲,它决定分配内存时上边界在哪。bottom_up表示分配方向:是从低地址往上,还是从高地址往下。

初始化时,这两个区域数组并不是动态申请的,而是先用静态数组顶着:

static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;

INIT_MEMBLOCK_REGIONS默认是 128。也就是说,在内核启动初期,memblock 最多只能记录 128 个 memory 区域和 128 个 reserved 区域,一旦超出就得扩容,这个扩容过程本身也是从 memblock 自己身上“割肉”,其中有不少坑,后面细说。

1.3 从汇编到start_kernel:memblock什么时候开始登场

很多人以为 memblock 是从 C 代码的setup_arch才开始用的,其实它“登场”得比想象中早。以 ARM64 为例,上电后先跑汇编代码(stext),那段汇编的主要任务是:确定内核加载地址、建立早期页表、打开 MMU、设置栈指针,然后才跳到start_kernel。

跳到start_kernel之后,setup_arch会被调用,这里面藏着 memblock 初始化的真正咽喉。ARM64 的setup_arch大致长这样:

void __init setup_arch(char **cmdline_p) { setup_machine_fdt(__fdt_pointer); ... arm64_memblock_init(); ... paging_init(); ... bootmem_init(); }

这几个调用顺序特别重要。setup_machine_fdt负责解析设备树,把物理内存信息塞给 memblock;arm64_memblock_init负责把内核镜像、设备树、initrd 这些关键资源在 memblock 里标注“已占用”;paging_init开始分配页表内存,这是 memblock 第一次大规模干活;bootmem_init则负责最后把内存信息整理好,准备交给伙伴系统。

所以,memblock 的整个生命周期大约就在setup_arch到memblock_free_all之间,时间不长,但干的都是地基的活,任何一个环节出错,后面启动必然翻车。

2. 初始化链条:内存信息怎么一步步进memblock

知道了大框架,再看细节。这一节我会沿着 ARM64 的启动路径,把内存信息从固件到 memblock 的每一步拆开,讲清楚谁提供数据、谁转换数据、谁消费数据。

2.1 固件与设备树:内存地图从哪来

物理内存的信息不可能凭空产生,源头有两个:传统的 BIOS/UEFI 固件映射,或者设备树。桌面和服务器上,UEFI 固件在启动时会维护一套内存描述表;嵌入式平台上,bootloader 会把设备树二进制(DTB)传给内核。

设备树里的关键节点是/memory,每个子节点带device_type = "memory"和reg属性,例如:

memory@40000000 { device_type = "memory"; reg = <0x0 0x40000000 0x0 0x80000000>; };

这表示从物理地址0x40000000开始,长度0x80000000(2GB)的内存。内核在early_init_dt_scan_nodes里会遍历这些节点,对每一段调用memblock_add(base, size),把内存区域加入memory列表。

这里有个容易被忽略的细节:设备树描述的是“整个物理内存”,但其中有些地址可能被外设映射占用、或者被 bootloader 保留。因此memblock_add并不会把所有区域都视为可用,后续其他模块还会调用memblock_reserve把这些坑标注出来。

除了设备树,EFI 系统也会提供内存图。支持 EFI 启动的 ARM64 平台上,efi_init会把 EFI 内存表中的EfiConventionalMemory等类型转换为 memblock 的 memory 区域,同时对 boot services 代码数据区域做 reserve。这个过程本质就是把“固件眼中的内存布局”翻译成“内核能理解的内存布局”。

2.2 arm64_memblock_init:内核给自己“圈地”

内存地图有了,下一步就是圈地。arm64_memblock_init这个函数虽然在 ARM64 架构代码里定义,但它干的活具有普遍性,主要是给四类东西“上锁”:

第一,内核镜像本身。内核代码段、数据段编译后是一整块二进制,加载到物理内存后,这段区域必须被保护起来,否则 memblock 可能把它分配给别的模块,那就天下大乱了。代码里通常是:

memblock_reserve(__pa_symbol(_text), _end - _text);

__pa_symbol(_text)是内核起始物理地址,_end - _text是镜像大小。一次 reserve,内核自己住的房子先保住。

第二,设备树二进制。bootloader 传进来的 DTB 本身占了一块内存,在内核用完它之前不能分给别人,所以也要 reserve。第三,initrd 镜像。如果启动时用 initrd 作为临时的根文件系统,这段内存也要单独锁住,直到内核把里面的内容解压处理完。

第四,也是比较隐蔽的,reserved-memory节点。设备树里可以声明一块内存专门给某个设备用,比如给 GPU 或 DSP 预留。这些区域经过early_init_fdt_scan_reserved_mem会全部变成 memblock 的 reserved 区域。

另外,如果开启了 KASLR,内核镜像的实际加载地址可能是随机的,代码里还会保留与随机化相关的一片区域。总之,arm64_memblock_init做完之后,关键资源的“所有权”都清楚了。

2.3 paging_init:页表也要从memblock借地皮

arm64_memblock_init之后是paging_init。这个函数负责建立完整的页表映射,而页表本身也是内存,必须从 memblock 拿。换句话说,memblock 在前面“圈地盖房”,现在开始“批地建基础设施”。

ARM64 下页表分四级:PGD、PUD、PMD、PTE。每一级页表的大小通常是 4KB 或 16KB,建立完整映射需要的页表数量相当可观。这些页表内存的申请,最后都会落到类似这样的调用:

pgd_t *pgd = pgd_pgtable_alloc();

pgd_pgtable_alloc内部其实是memblock_alloc的包装。这里有一个很重要的设计理念:页表内存一旦分配,生命周期跟内核一样长,几乎不会释放,所以直接通过 memblock 分配是完全合理的。

我遇到过一种典型问题:某个平台的外设挂在一片地址空间里,但设备树的 memory 节点没有把外设区域排除干净,paging_init建立线性映射时给外设地址也做了内存映射,随后 memblock 把那些物理页当普通内存交给伙伴系统,最后驱动的寄存器读写变得异常,整一个“灵异事件”。这种问题排查特别痛苦,后来我们养成了习惯:每次 merge 设备树改动,都会先拿memblock=debug启动参数过一遍,确认 memory 和 reserved 区域没有覆盖到外设保留地址。

2.4 一个容易被忽略的变量:current_limit

memblock.current_limit这个字段,很多讲内存初始化的文章一句话带过,但实际排障时它非常重要。它的含义是:“当前允许分配内存的最大物理地址”。

什么情况下会限制分配地址?最常见的是 DMA 能力有限的设备,或者某些平台的外设地址映射只支持有限的地址范围。比如某颗芯片的 DMA 控制器只能寻址低 1GB 物理内存,如果内核把驱动缓冲区分配到高地址,DMA 就写不到。于是arm64_dma_init这类早期初始化函数会调用memblock_set_current_limit把上限压低。

还有一个场景是bootmem_init阶段计算max_pfn时,需要知道当前可用的内存上边界。如果 current_limit 设置不当,memblock 的分配结果可能会超出预期的物理范围,导致后续访问非法地址。

说个实际案例:某次移植新平台,内核启动到一半直接 panic,日志显示memblock_alloc: failed。反复查发现是setup_arch里某个外设驱动初始化提前执行,把 current_limit 设成了一个很小的值,而页表分配需要的内存范围远超这个限值,于是分配失败。解决办法就是在正确的位置设置限制,并且预留足够的页表空间。

3. 核心API怎么工作:插入、合并、预留、分配

初始化过程离不开几个核心 API。光知道“调用它会怎样”是不够的,我建议每个做启动层开发的人都把memblock_add、memblock_reserve、memblock_alloc、memblock_free的实现逻辑读一遍。这一节我把它们的内部机制挑重点讲。

3.1 memblock_add与区域合并算法

memblock_add的底层是memblock_add_range,做的事情可以概括为四步:遍历现有区域、判断插入位置、处理重叠、合并相邻区域。

假设 memory 列表里已经有了[0x40000000, 0x80000000),现在要插入[0x40000000, 0x40002000),内核不会简单地把两个区域都留着,而是会尝试合并,让列表保持简洁。如果新区域落在某个旧区域内部,更新旧区域的边界;如果相邻,直接连起来。

合并算法使用了一个技巧:先把需要插入的区域拆分成多个片段,再逐个调用memblock_insert_region插入到正确位置。这个过程中,如果发现cnt == max,也就是静态数组满了,就会触发扩容逻辑,也就是memblock_double_array。

这里要提一个常见的坑:很多人会在设备树里写多个不连续的/memory节点,每个节点长度很小,比如一堆 4MB 的碎片。这样会导致 memory 列表的 cnt 快速增长,很快就达到 128 的上限。扩容逻辑虽然存在,但扩容时机如果太早,可能连新数组的内存都申请不到。所以在早期设备树里,我会尽量合并相邻的 memory 节点,减少碎块数量。

还有一种情况也容易踩:memblock_add在 64 位平台上接受phys_addr_t,但很多人在设备树里写的reg是 32 位长度,导致高地址内存被截断。特别是内存超过 4GB 的板子,必须写成<0x0 0x40000000 0x1 0x00000000>这种两段式表示法。

3.2 memblock_reserve的“圈地运动”

memblock_reserve与memblock_add流程类似,但它操作的是 reserved 列表。调用它的时候,reserved 区域会以独立列表的形式记录,和 memory 列表互不干扰。分配内存时,memblock 会同时检查两个列表。

有任何一段物理地址被 reserve 之后,memblock_find_in_range在找可用区域时就会自动跳过它。这个机制保证了“先到先得,重复使用必冲突”的早期内存秩序。

有一个细节我觉得值得记住:memblock_reserve并不会自动把对应区域从 memory 列表中删除。换句话说,memory 列表可能依然包含那块地址,只是它“不可被分配”了。直到memblock_free_all阶段,for_each_free_mem_range在生成空闲页时,会减去所有 reserved 区域,最终伙伴系统拿到的内存才是真正可用的。

为什么不直接删掉?因为保留一块区域不代表它永远不可用,比如某个驱动在初始化完成后可能释放它,那时就需要把这块区域从 reserved 中移除,回到可用池。保留原始 memory 信息,加上独立的 reserved 列表,操作更加灵活。

3.3 memblock_alloc的分配路径与对齐

memblock_alloc的内部路径我建议记成一句话:先在 current_limit 范围内找一块满足对齐和大小要求的空闲区域,然后立刻把它 reserve。

分配接口有几个变体,常用的是:

void *memblock_alloc(phys_addr_t size, phys_addr_t align);

align是对齐要求,比如SMP_CACHE_BYTES是 64 字节,页表分配通常用PAGE_SIZE。对齐越严格,能匹配的区域就越少,内存浪费也会增多。举一个实践例子,某个平台要分配一块 DMA 缓冲区,要求 1MB 对齐,结果在地址空间里找来找去只有几块碎片满足,最后分配失败。这种场景下,我会降低对齐要求,或者在启动参数里调整内存布局。

memblock 还支持两种分配方向:bottom_up和top_down。默认多数架构是 top_down,也就是从高地址往下找。这样做的好处是尽量保留低地址的连续内存,方便设备 DMA。有些场景需要反过来,比如早期某些必须分配低地址的情况,就可以用memblock_set_bottom_up(true)切换。

memblock_alloc还有一个兄弟memblock_alloc_node,专门在指定 NUMA 节点上分配。早期 NUMA 平台上,节点间的内存拓扑会影响分配结果,如果开始没指定 nid,后续可能埋下性能隐患。

3.4 区域数组溢出怎么办(memblock_double_array)

前面提到静态数组容量有限,一旦溢出就要扩容。memblock_double_array做的事是申请一块新内存,把旧的memblock_region数组内容复制过去,然后让memblock_type.regions指向新数组。

问题来了:申请新数组本身用的是什么函数?还是memblock_alloc。这就形成一种“鸡生蛋、蛋生鸡”的困境:数组满了是因为内存区域太多,而扩容又需要再分配一块内存,这块新内存也必须记录在同一个列表里,可能会再次触发数组溢出。

实际实现里,内核通常先把新数组放到临时位置,再手动更新列表,降低递归风险。但作为使用者,最好别把这事拖到极限。我调试过一个平台,因为设备树里 reserved-memory 节点数量太多,导致 reserved 列表溢出。当时串口打印显示memblock_add_range失败,后续内存分配全部异常。我的处理方式是:把多个 reserved-memory 节点合并成一个大节点,并在内核配置里调大INIT_MEMBLOCK_RESERVED_REGIONS。

3.5 memblock_free其实只是解锁

memblock_free这个名字有迷惑性,它并不会把内存还给 buddy 系统,而是把区域从 reserved 列表中移除,让它重新变成“可被 memblock 分配”的状态。

内部实现大概是:

int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size) { return memblock_remove_range(&memblock.reserved, base, size); }

和memblock_reserve正好互为逆操作。早期内核里有些临时分配的内存(比如某段临时页表)用完之后会调用它来释放。

要注意一个语义陷阱:如果某块内存从来就没有被 reserve,memblock_free并不代表真的能释放任何东西。另外,如果内存区域已经被用来建立页表映射,就算 memblock 层面解锁,MMU 映射仍然存在,直接复用那块地址会导致数据错乱。所以释放的时机和调用方必须严格约定。

4. 交接班:memblock_free_all与伙伴系统

memblock 终究只是过渡方案,等到伙伴系统准备好,memblock 就必须把“所有可用内存”一次性完整交出去。交接的核心函数是memblock_free_all。

4.1 free_low_memory_core_early做了什么

memblock_free_all的实现在mm/memblock.c,它最终会调用free_low_memory_core_early。这个函数的关键逻辑是遍历 memory 列表中的所有区域,减去 reserved 区域,对剩下的每段空闲内存调用__free_pages_bootmem,逐页释放到伙伴系统。

简化理解就是:

for_each_free_mem_range(i, NUMA_NO_NODE, MEMBLOCK_NONE, &start, &end, NULL) count += __free_memory_core(start, end);

这里的__free_memory_core会把 [start, end) 按MAX_ORDER的粒度拆分成若干连续的页块,然后添加到 buddy 的 free list。这一步完成后,kmalloc、alloc_pages等常规内存分配接口才真正可用。

我建议你在自己的移植代码里,跑完memblock_free_all后打印一下释放的总页数,和板子的物理内存总量做对比,如果少了很多,说明 reserved 区域占用了过多内存,比如 CMA 预留区域配置太大、或者多个驱动保留了不必要的区域。

4.2 空洞、NUMA与“不能交给伙伴系统的内存”

真实平台的内存布局往往不是干净的连续区间,会在中间夹着外设映射、保留区域、空洞地址。for_each_free_mem_range会自动跳过这些空洞,所以伙伴系统最终拿到的页描述符(struct page)数组并不完全连续。

这里牵出一个重要环节:建立struct page数组的sparse_init或paging_init阶段,需要先确定每个内存节点的pgdat结构、页描述符区间等。这些元数据自身的分配,也发生在 memblock 时代。也就是说,为了把内存交给伙伴系统,内核得先“花掉”一小部分内存来维护伙伴系统的数据结构。

NUMA 平台上更麻烦。每个节点有自己的内存范围,memblock 的 memory 列表里,每个 region 都带nid字段。memblock_free_all在做for_each_free_mem_range遍历时,会区分节点,然后分别把空闲页放到对应节点的 buddy 系统里。如果设备树或者 ACPI 表里没有正确描述 NUMA 拓扑,可能出现内存全部归到一个节点的情况,性能大打折扣。

我还踩过一个和“可移动区域”相关的坑:内存热插拔要求部分内存标记为MEMBLOCK_HOTPLUG或类似属性,如果 reserve 区域和普通 memory 区域边界计算错误,热插拔时会出现引用已经移走的struct page,直接内核 panic。

4.3 交接之后memblock还在吗

memblock_free_all执行后,伙伴系统接管一切,但 memblock 并没有完全消失。它作为内核的早期内存分配器,仍然保留在代码里,只是绝大多数内存分配不再走它。

某些特殊场景还会用到 memblock:比如内核启动早期调试、内存热插拔时的临时处理、以及CONFIG_ARCH_KEEP_MEMBLOCK选项打开时,内核会保留 memblock 的完整状态,允许驱动在运行时查询“哪块内存被谁保留了”。这在调试启动阶段的内存踩踏问题特别有用。

平时我们更常用的是/proc/iomem,它其实就源自 memblock 保留的 memory/reserved 信息。启动之后执行cat /proc/iomem,能看到系统当前的内存布局、内核镜像位置、设备保留区等,这在排查设备驱动地址冲突时几乎是第一利器。

5. 排障黑科技:让memblock说实话

memblock 在启动阶段的所有操作默认是不打印的,因为内核在早期还没有完善的日志系统。但它的调试机制很简单实用,掌握几个小技巧,排查启动问题能省一半时间。

5.1 memblock=debug开关

内核启动参数可以传memblock=debug,加上之后,memblock 的每一次 add、reserve、alloc、free 操作都会通过memblock_dbg打印出来。串口启动日志里会出现类似:

memblock_add: [0x0000000040000000-0x00000000bfffffff] memblock_reserve: [0x0000000040800000-0x0000000040a00000] memblock_alloc: [0x00000000bfe00000-0x00000000bfffffff]

这些打印信息能看到每个操作发生的顺序、地址范围,还能顺便看到是哪个函数触发的。如果某块内存被莫名其妙覆盖,从日志里找到最后一次memblock_reserve或者memblock_alloc的位置,顺着调用栈就能定位到肇事者。

我调试内存踩踏问题时,经常先开memblock=debug,对比正常板子和异常板子的日志差异。通常异常板子在某一时刻多了一次memblock_reserve,或者多了一次 unexpected 的memblock_alloc,问题就水落石出了。

5.2 常见翻车现场与排查思路

整理几个我实际遇到或者反复看到的高频问题,做成速查:

内存总量对不上:memblock_free_all释放的页数明显少于预期。大概率是设备树 memory 节点缺失、CMA 预留过大、或者 reserved-memory 节点重复添加。先看memblock=debug日志里的 memory/reserved 区域列表,再用/proc/iomem二次确认。

memblock_alloc 失败:日志显示memblock_alloc: Failed。常见原因有三种:内存碎片过多导致找不到满足对齐的区域;current_limit 设置过高或过低,限制了分配范围;数组扩容失败。第一步就是打印memblock.current_limit和每个分配请求的 size/align,对比一下实际空闲区域。

启动早期数据被篡改:如果现象是某个全局变量或页表项在启动中被改掉,多半是某段内存被二次分配。检查 reserved 列表是否包含对应的物理地址,尤其是设备和固件保留区域经常被遗漏。

设备树修改后启动失败:这种情况先怀疑/memory节点与外设地址重叠。在arm64_memblock_init里添加临时打印,输出 memory 列表的所有 region,和外设手册对照。

表格形式可能更直观:

现象排查优先级最可能的根因
memblock_alloc失败看日志里的分配参数对齐过高、current_limit限制、碎片化
物理内存总量减少对比总大小与free_all页数memory节点漏配、CMA过大
早期数据被篡改查reserved列表与调用栈二次分配、保留区域遗漏
启动流程卡死在paging_init检查页表分配是否成功数组扩容失败、内存碎片

5.3 自己动手实验:加打印看初始化全过程

如果你是第一次接触这块,光看文档容易感觉空泛。我建议在arm64_memblock_init和paging_init之间加一行临时打印,输出memblock.memory.cnt、memblock.reserved.cnt和当前 current_limit:

pr_info("XXX: mem=%lu res=%lu limit=%pa bottom_up=%d\n", memblock.memory.cnt, memblock.reserved.cnt, &memblock.current_limit, memblock.bottom_up);

启动后对照日志,你会看到 memory 区域数量、reserved 区域数量以及分配方向。然后在memblock_free_all之前再打印一次,两次的差值就是启动过程中分配出去的内存总量。

再用一个实验验证memblock_free的语义:在某个早期驱动里手动memblock_reserve一块区域,打印分配结果,再memblock_free它,再分配一次同样地址,看看能否成功。这个实验能直观感受到 reserved 列表的“锁”和“解锁”行为,比看十篇文档都管用。

我在实际项目里的体会是:memblock 的调试手段不需要太高深,关键是把“谁在什么时候占了哪块内存”这个问题记录清楚。memblock=debug加上几次手动打印,就能把启动早期那段“混沌时期”照亮大半。最后再分享一个小技巧:如果你频繁调试内存布局,可以在自己的板级初始化代码里固定一段memblock_reserve作为“调试保护区”,比如 reserve 一个 4KB 区域,然后在怀疑踩踏的驱动里尝试访问它,一旦发现内容被改,说明刚好有模块越界写到了这个保护区。这种“诱饵内存”的做法,我在移植新平台的早期阶段基本是标配。

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

SpringBoot+Vue+MySQL进销存系统设计与实现全解析

1. 项目整体拆解与技术选型分析1.1 进销存系统究竟在解决什么业务问题做毕业设计最怕选到一个“看起来简单、实际写起来全是坑”的题目&#xff0c;进销存系统恰好属于那种“业务场景清晰、技术覆盖全面、答辩时还能讲出深度”的典型题目。先说清楚一个多数同学容易忽略的点&am…

作者头像 李华
网站建设 2026/10/16 8:25:44

airllm实战:大模型推理如何突破显存与长上下文限制

我最早注意到 airllm 这个项目&#xff0c;是一次在本地跑一个 7B 参数模型时不断撞上显存不足的报错。翻来覆去找方案&#xff0c;看到了这个轻量推理框架&#xff0c;跟我之前用过的那些全家桶式框架完全不是一个路数。它没有铺天盖地的宣传&#xff0c;也没有复杂的配置&…

作者头像 李华
网站建设 2026/10/16 8:24:49

PyTorch入门到进阶:动态图调试与自动求导实战指南

开门见山说句我的真实感受&#xff1a;PyTorch 用到现在&#xff0c;我对它的评价一直是八个字——调试友好&#xff0c;心态不崩。当年从别的框架切过来的时候&#xff0c;最打动我的不是宣传材料里那些“高效”“灵活”的形容词&#xff0c;而是真正的使用体验&#xff1a;在…

作者头像 李华
网站建设 2026/10/16 8:24:24

0.4B对话模型从零训练实战:数据清洗、分词器与训练配置全解析

1. 为什么一个 0.4B 的对话模型值得从零训一遍先把结论放在前面&#xff1a;本文说的“从零训练”&#xff0c;指的是不加载任何预训练权重、分词器也是自己训练的那种从头开始。模型规模控制在 0.4B 参数附近&#xff0c;训练数据规模在 1B tokens 左右&#xff0c;单张 24G 显…

作者头像 李华
网站建设 2026/10/16 8:24:24

多智能体协作系统架构设计与落地:从单体Agent到Agency Agents

最近差不多两个月的时间&#xff0c;我一直泡在一个内部研发辅助工具的重构项目里&#xff0c;核心动作就是把它从单Agent模式改成多智能体协作系统&#xff0c;内部代号就叫Agency Agents。听着有点绕&#xff0c;但整体思路其实很朴素&#xff1a;与其让一个大模型从头到尾包…

作者头像 李华
网站建设 2026/10/16 8:23:35

STM32 FreeModbus-RTU主机模式数据接收函数传递全解析

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

作者头像 李华