读完这篇,你能做什么
能为一条公式列出变量、输入范围、单位和示例,并检查张量元素数、字节数在边界输入下是否算对。
学习理由、准备要求与知识点
为什么学它
字母认错或单位没对齐,计算和资源估算就可能差很多。先能把公式中的每个数量说清楚。
先读这些内容
这篇可以直接开始,不需要先读其他笔记。
知识点清单
- 变量与常量
- 函数输入和输出
- 定义域与取值范围
- 下标与求和
- 指数与科学计数法
- 量纲与单位换算
- GB 与
GiB - 整数溢出和估算边界
运行前先确认Java 17;Python 3.14 CPU 教学实验,无外部数据、预训练权重、GPU 或多机训练。源代码基线与局限见源码模块。
01先弄懂它是什么
读公式时,可以先问:给它什么数,它按什么规则算,最后得到什么?在 y=f(x) 中,x 是输入,f 是处理规则,y 是输出;括号表示把 x 交给 f。比如 f(x)=2x+1,输入三就得到七,输入负一就得到负一。还要看哪些输入允许使用:在实数范围内求平方根,输入就不能是负数。
同一个字母在不同文章里可能代表不同东西,所以先看作者的说明。本篇用 n 表示样本数,d 表示每个样本的特征数,x_i 表示第 i 个样本或分量。比如记录学生成绩,i 就是用来区分第几个学生的编号。数学下标常从一开始,程序数组常从零开始,转换时别多算或漏算一项。sum(i=1..n, x_i) 的意思是把这 n 项依次加起来。
数量还带有单位。训练耗时可以用秒,数据大小用字节,吞吐量用样本每秒。速度乘时间得到样本数,单位相除后应与输出匹配。把每秒处理一千个 token 与每秒处理一千个请求直接比较没有意义,因为一个请求的 token 数可能不同。建立变量表和单位表,是理解性能图与研究公式的第一步。
文中这几个词,先认一认
- 定义域
- 函数可以接受哪些输入。例如实数平方根函数只接受非负数。
- 量纲
- 一个数在计量什么,例如时间、存储大小或样本数量。比较和运算前要先弄清这一点。
- 数量级
- 一个数大约有多大,常用十、百、千或十的幂来表示,方便检查结果有没有差得太远。
02看看它怎么工作
从能手算的小例子开始:一个表有三行、四列,共十二个位置;若每个位置用四字节表示,仅元素存储需要四十八字节。推广到形状 [n,d],元素数 N=n*d,元素字节数 M=N*b,其中 b 是每元素字节数。这里 M 不是整个训练进程内存,它没有包含梯度、优化器状态、临时激活、容器与运行时开销。
科学计数法把三百万写为 3*10^6,便于比较和换算。指数乘法规则作用于相同底数:10^3*10^6=10^9;它不代表把指数随意相乘。增长从一百万到两百万是增加一百万,也是增加百分之一百;从准确率百分之八十到百分之八十五是增加五个百分点,相对提升百分之六点二五。论文图表里的百分比必须先识别分母。
单位前缀也要明确。GB 通常表示 10^9 字节,GiB 表示 2^30 字节,因此十亿字节约为 0.931 GiB。数值不同不一定意味着工具统计出错,可能只是换算基数不同。先统一单位再讨论差异,并检查一个字段到底记录元素个数、字节还是位;一字节通常为八位,本篇存储估算以字节为单位。
程序中的整数有有限范围。Java long 最大值是 2^63-1,普通乘法溢出后不会自动报错,而资源估算可能因此得到负数或很小的正数。Python 整数可以扩展位数,但它仍受内存与计算资源约束。用于精确计数时应保留整数并检查边界,不能先转 double 再期待所有大整数都精确可辨。
首次运行实验时,把 Python 代码保存为 experiment.py,在文件所在目录执行 python experiment.py;程序完成四组断言后打印 PASS。Java 部分保存为 Main.java,用 javac --release 17 -encoding UTF-8 Main.java 编译,再运行 java Main。先核对小例子,再观察零维度、空形状和溢出案例,不需要先安装任何神经网络库。
先跑通一个最小例子
stdlib CPU 实验:验证元素数、字节换算和空形状约定。Python 的任意精度结果用来对照 Java long 的有限范围。
import math
def storage(shape, item_bytes):
if type(item_bytes) is not int or item_bytes <= 0:
raise ValueError("item_bytes")
if any(type(d) is not int or d < 0 for d in shape):
raise ValueError("shape")
return math.prod(shape) * item_bytes # 空乘积为一,对应标量的一个元素。
assert storage([3, 4], 4) == 48
assert storage([], 8) == 8 # 空形状不同于含零长度轴。
assert storage([0, 100], 4) == 0
assert math.isclose(10**9 / 2**30, 0.9313225746154785)
assert storage([2**40, 2**30], 8) == 2**73 # 数学结果存在,但超过 Java long。
try:
storage([-1, 3], 4)
raise AssertionError("negative dimension accepted")
except ValueError:
pass
print("PASS 6: shape, units, zero, scalar, large integer, invalid input")
核对运行结果打印 PASS 6;10^9 字节换算约为 0.9313 GiB。这里只计元素载荷,没有测量真实训练内存。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["写清变量和允许输入"] --> B["核对维度与单位"]
B --> C["先算小例子"]
C --> D["实现边界检查"]
D --> E["解释结果与未计入成本"]按顺序看,每一步做了什么
- 为每个符号写出含义与单位。
- 列出输入范围及空值约定。
- 用小整数手算预期结果。
- 实现精确计数及溢出检查。
- 把估算值与真实测量的范围区分。
03对照实现代码
本机 Microsoft OpenJDK 17.0.20.1;上游 jdk-17.0.20-ga · java.lang.Math#multiplyExact(long,long)
下面是原创 Python 非负整数乘法边界模型,不是原样 JDK Java 源码;只解释本篇资源计数需要的非负分支,不模拟全部有符号位运算。
def checked_nonnegative_product(a, b, maximum):
if a < 0 or b < 0:
raise ValueError("nonnegative only")
if b and a > maximum // b: # 先用除法检查,避免先产生不可表示的乘积。
raise OverflowError("product")
return a * b # 零乘积自然为零;真实 JDK 还覆盖有符号极值。
真实 Math.multiplyExact(long,long) 先计算机器乘积,再结合操作数高位和除法关系检查溢出,另行处理最小负数乘负一的特殊情况。它与普通乘法返回值不同的关键在于越界时抛 ArithmeticException,而不是把回绕结果当成正确计数继续使用。
本篇只接受非负维度,因此教学模型可以使用 a > maximum // b 判断乘积是否越界,并先排除 b 为零。真实源码采用适合完整 long 域的实现,不能把非负模型用于负数后声称完全等价。说明输入域,才能判断精简实现保留了哪部分语义。
一次乘法检查只证明这个中间结果能装入 long,不能证明最终内存可以分配。机器可能没有足够可用内存,数组实现也可能有更严格的长度限制。资源规划中的数字边界与实际分配边界要分开记录,避免把算法返回一个正数解释成训练一定能启动。
源码阅读还提醒我们区分数学函数和机器实现:数学整数乘法总有整数结果,程序函数却可能因为表示范围拒绝输入。这个拒绝是契约的一部分。算法把异常用于指出无法可靠表示的估算,不捕获后随意返回零,因为零会被误解为资源需求不存在。
代码先调用谁,再调用谁
- 定义非负维度与每元素字节数
- 逐项检查维度
- 乘积进入
multiplyExact - 越界抛异常或返回精确计数
- 按声明的单位展示结果
04动手写一个例子
含零维度的安全存储计数
形状使用非负 long 数组,每元素字节数为正数;空形状表示一个标量,含任意零轴则总字节为零。结果超出 long 时明确抛异常,不分配实际数据。
- 输入 1
shape=[3,4], bytes=4- 输出 1
48
- 输入 2
shape=[], bytes=8- 输出 2
8
- 输入 3
shape=[MAX,MAX,0], bytes=4- 输出 3
0
先完整扫描形状,拒绝负维度并记录是否含零。为什么不能边乘边遇零再返回?因为 [MAX,MAX,0] 的数学乘积为零,若前两项先乘可能提前溢出;反过来一看到零就返回,也会漏掉后面的负维度。先校验整个输入,再处理零,能同时满足两种边界。
无零维度时,从一开始累乘,空形状自然保留一,最后再乘每元素字节数。每次使用 multiplyExact,保证一旦返回结果,就没有任何一步悄悄回绕。这里不使用浮点数,因为精确元素计数不应随着数量变大而丢失个位信息。
正确性可以按处理前缀归纳:初始一是空乘积;加入下一个维度后,若未抛错,新状态就是扩展前缀的精确乘积。第二次扫描只在全部维度为正时进行,所以中间乘积不会先越界再因为后续零缩小。测试包含普通、标量、零、零后非法和真实越界。
为什么这样写是对的进入乘法阶段时全部维度合法且为正;每次成功更新后保存已处理前缀的精确 long 乘积。
先检查 k 个维度,再至多乘 k 次;每次 long 运算在固定机器字长下为常数。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
public class Main {
static long storage(long[] shape,long bytes) {
if(shape==null || bytes<=0) throw new IllegalArgumentException("input");
boolean zero=false;
for(long d:shape) {
if(d<0) throw new IllegalArgumentException("dimension");
if(d==0) zero=true; // 先完整校验,零不能掩盖后面的负维度。
}
if(zero) return 0; // 数学乘积为零,不应被前面巨大维度误判为溢出。
long count=1; // 空形状代表标量,空乘积为一。
for(long d:shape) count=Math.multiplyExact(count,d);
return Math.multiplyExact(count,bytes); // 字节换算本身也可能越界。
}
static void eq(long a,long b){if(a!=b)throw new AssertionError(a+" != "+b);}
static void fails(Class<? extends Throwable> type,Runnable r){
try{r.run();}catch(Throwable e){if(type.isInstance(e))return;throw new AssertionError(e);}
throw new AssertionError("expected "+type);
}
public static void main(String[] args){
eq(storage(new long[]{3,4},4),48);
eq(storage(new long[]{},8),8);
eq(storage(new long[]{Long.MAX_VALUE,Long.MAX_VALUE,0},4),0);
fails(IllegalArgumentException.class,()->storage(new long[]{0,-1},4));
fails(ArithmeticException.class,()->storage(new long[]{Long.MAX_VALUE,2},1));
fails(IllegalArgumentException.class,()->storage(new long[]{1},0));
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 空形状与零长度轴不同。
- 零轴不掩盖非法负维度。
- 最终字节乘法也检查溢出。
进阶选读 · 多项训练状态的内存预算
场景需要同时估计参数、梯度和优化器状态。
做法为每一项单独定义元素数量和存储精度,精确乘加后再留出激活与临时缓冲空间;用真实 CPU/GPU 测量校准。
复杂度项数与维度总数线性;实际框架内存还受存活期影响。
好处与代价静态估算便于排除明显不可行配置,但不能替代目标设备测量。
05项目里怎么用
研究记录应该同时写明公式、单位和统计范围。例如显存峰值、常驻参数内存与进程总内存不是同一指标,比较时要固定批大小、序列长度和精度。若只展示一个数字而没有分母与测量区间,读者很难判断提升来自算法还是配置变化。
近似估算和精确计数有不同目的。早期规划可以把若干小项合并为保守余量,定位超大成本;落到可执行配置时,则需要检查具体形状与整数范围。有效数字过多并不使一个忽略重要项目的估算更准确,反而可能制造不必要的确定感。
从公式读到代码时,先检查索引范围和边界,再讨论性能。求和的一到 n 与数组零到 n-1 对应同样 n 项;误写到 n 会越界,少写一项则悄悄改变统计量。建议在实验笔记中保留一个手算样例和一个边界样例,使后续迁移到 Tensor 或批处理时仍有可比较的基准。
容易踩的坑
- 把
f(x)当成 f 乘 x。 - 混淆百分点与相对百分比。
- 将 GB 和
GiB直接比较。 - 用普通整数乘法估资源而忽略溢出。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 整数精确计数 | 元素和字节可严格核对 | 受目标整数表示范围限制 | 形状与容量检查 |
| 浮点数量级估算 | 便于快速比较大规模配置 | 可能舍入且依赖遗漏项目 | 规划早期 |
| 真实运行测量 | 覆盖实现开销和存活期 | 需要目标环境且结果依赖配置 | 验证可执行预算 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01公式中 n 总表示样本数吗?
展开参考答案与推导
不总是。符号由当前文档定义,可能是维度、迭代数或别的数量。阅读时先建立本篇变量表,不能沿用另一篇论文的含义。
02为什么空形状元素数是一?
展开参考答案与推导
形状没有轴时可表示标量,乘积的单位元是一;这与某个轴长度为零、实际没有元素的情形不同。接口必须明确采用哪种约定。
03十亿字节为何小于一 GiB?
展开参考答案与推导
GiB 的换算分母为二的三十次方,比十亿大;相同字节数换算后约为零点九三一 GiB。差异来自单位定义,而不是凭空丢失数据。
换个情况,还会处理吗
01预计参数只占四 GB,训练为何还需更多内存?
展开参考答案与推导
参数载荷只是部分状态,梯度、优化器、激活和临时缓冲都可能占空间。先列出估算覆盖项,再测完整运行峰值,不能由单项结果推导进程总需求。
02准确率从八十到八十五如何表述?
展开参考答案与推导
应说提高五个百分点;相对原值的提高是五除以八十,即百分之六点二五。写清分母可以避免让同一次变化看起来像不同幅度。
03资源计数变成负数应如何排查?
展开参考答案与推导
核对是否发生固定宽度整数溢出,检查每一项维度及单位,再使用精确乘法或更大整数表示。不要简单取绝对值,那只会掩盖错误。
写代码试一试
01计算元素载荷。
- 输入 / 条件
形状 [2,3,5],每元素 4 字节- 输出 / 预期
120 字节
提示 先乘元素数。
展开参考答案与推导
三十个元素乘四字节等于一百二十字节。这只是给定数组的元素载荷,不包含容器头、对齐和训练附加状态。
02处理后置零维度。
- 输入 / 条件
[MAX,MAX,0]- 输出 / 预期
0
提示 先完整验证再乘。
展开参考答案与推导
确认所有维度非负后,任何零轴使乘积为零;无需先计算可能溢出的前缀。仍要检查零后面的维度是否合法。
03转换吞吐率。
- 输入 / 条件
600 个样本用时 3 秒- 输出 / 预期
200 样本/秒
提示 数量除时间。
展开参考答案与推导
将样本数除以秒数得到每秒样本数量;若各样本 token 数不同,这个指标不能直接转换成 token 吞吐而不补充长度信息。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能为一条公式列出变量、输入范围、单位和示例,并检查张量元素数、字节数在边界输入下是否算对。
本篇重点加练
函数组合的合法范围和单位一致性决定公式能否安全转成程序,值得反复核验。
- 定义域与取值范围
- 量纲与单位换算
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
为函数
f(x)=sqrt(12-x)和g(x)=1/(x-2) 分别写实数输入契约,并区分表达式中的常量、变量与函数值。- 提交内容
- 两份函数契约与合法性表
- 怎样算完成
- 输入集合表达完整
- 边界依据明确
练到的知识点变量与常量 · 函数输入和输出 · 定义域与取值范围
- 作业 02代码推演
数组 a=[2,-1,4,0] 上,比较从 i=1 到 4 累加
a_i与 Pythonrange(1,4)访问 a[i] 的含义;重新写出两种索引约定的完整计算记录。- 提交内容
- 索引对应表与求和记录
- 怎样算完成
- 数学及程序下标不混用
- 所有访问可追踪
练到的知识点下标与求和
- 作业 03原理问答
设
f(x)=sqrt(x)、g(x)=x-3,分别为 f(g(x)) 与 g(f(x)) 写输入契约;检验交换组合顺序是否保留合法输入集合。- 提交内容
- 两份契约和集合关系图
- 怎样算完成
- 每层输入限制均有记录
- 组合与单函数范围区分
练到的知识点定义域与取值范围
- 作业 04代码推演
同一阶段耗时记录分别为 2 秒、800 毫秒,处理量分别为 100、80 个样本。比较“阶段速度的简单平均”与“总量除总耗时”的计算对象,自拟需要哪种指标的使用情境。
- 提交内容
- 带单位账本与指标说明
- 怎样算完成
- 两个统计对象定义清楚
- 每项量的分母可追踪
练到的知识点量纲与单位换算
综合应用
- 作业 05编程实践
用 Python 编写容量报告器,输入条数、每条元素数和每元素字节数,同时输出精确字节、GB、
GiB和科学计数法;测试 750000 条、每条 1536 个四字节元素。- 提交内容
- 脚本、单位说明及测试记录
- 怎样算完成
- 单位口径一致
- 精确值与近似值明确区分
练到的知识点指数与科学计数法 · 量纲与单位换算 · GB 与 GiB
- 作业 06设计评审
评审资源表:参数占用=参数数×字节数,训练内存=参数占用,吞吐=请求数/毫秒。为每项列出可支持的结论、遗漏范围及需要采集的量。
- 提交内容
- 资源表修订稿
- 怎样算完成
- 量纲自洽
- 估算范围和测量范围可区分
练到的知识点量纲与单位换算 · 整数溢出和估算边界
- 作业 07排错实验
构造 Java
int、long和 Pythonint对同一正整数乘积产生不同表现的最小复现实验,不申请相应大小的实际数组;记录程序如何识别超出接口范围。- 提交内容
- 三个计算记录与边界测试
- 怎样算完成
- 无巨量内存分配
- 边界值和异常行为可复核
练到的知识点整数溢出和估算边界
- 作业 08编程实践
仅用 Python 标准库编写批次耗时接口:n 为非负整数,r 为正数且单位为样本/秒,启动耗时单位为毫秒。自行定义空批次是否启动,测试合法值、负值和错误单位。
- 提交内容
- 接口定义、程序与测试
- 怎样算完成
- 空输入语义明确
- 单位及数值校验分别覆盖
练到的知识点定义域与取值范围 · 量纲与单位换算
进阶挑战 · 掌握前置后选做
- 作业 09面试场景
某实验从 800 个有效目标中答对 600 个,另一实验从 1200 个中答对 960 个。评审“提升 360 个,所以质量提升 60%”这一摘要,并制定允许跨实验比较的条件。
- 提交内容
- 一页指标审阅意见
- 怎样算完成
- 分母和比较对象明确
- 不能比较的部分说明充分
练到的知识点变量与常量 · 定义域与取值范围 · 指数与科学计数法 · 量纲与单位换算
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 跨域迁移
迁移到电池续航估算:自行定义容量、功率与续航公式及单位,加入零功率、待机耗电和容量衰减条件;哪些输入使原公式失去适用性?
- 提交内容
- 模型假设表与反常输入清单
- 怎样算完成
- 条件改变能对应具体失效
- 近似与精确关系分开
练到的知识点定义域与取值范围 · 量纲与单位换算
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
OpenJDK 17u:Math.multiplyExact 的溢出检查 (新标签页)↗
核对固定宽度整数乘法的越界处理,理解数学计数与机器表示的区别。
- 深入参考文档英文
OpenStax:函数与函数记号 (新标签页)↗
从输入输出和定义域开始理解函数符号。
- 深入参考文档英文
NIST:二进制前缀 (新标签页)↗
核对
KiB、MiB、GiB与十进制前缀的区别。 - 深入参考文档英文
Java 17 Math API (新标签页)↗
查阅
multiplyExact的抛错契约与数值边界。