faceage-onnx / README.md
imbcmdth's picture
faceage-dino-vitl16-fused-fp16.onnx: the same network rebuilt with fused attention, rotary and layer norm, for ONNX Runtime 1.22's CUDA provider
c795238 verified
|
Raw History Blame Contribute Delete
7.47 kB
metadata
license: apache-2.0
base_model: TrungTran/faceage_ClientScan
tags:
  - age-estimation
  - face-analysis
  - onnx
  - fp16
  - dinov3

faceage-onnx

FaceAge ClientScan halved to fp16 for use as an ffrwd model pin. The network and weights are TrungTran's, Apache-2.0: a DINOv3 ViT-L/16 backbone with a CORAL ordinal-regression age head and a gender head, trained on face crops. This repo holds two converted files of that network and the source's own configuration, nothing more.

Built with DINOv3. The backbone is Meta's, under the DINOv3 License, which travels with any derivative: a copy is in this repo, and anything that redistributes the model carries it too.

file bytes sha256
faceage-dino-fp16.onnx 611872451 b2e804783e197066cd8cf65a7cbc39a0579ea87c34660abd8ca942df8672cf70
faceage-dino-vitl16-fused-fp16.onnx 611187743 30c5531f17a1faa1f27891cb8af3e12f1fb14ea5f456192d0f1afcc8c9105ba0

faceage-dino-fp16.onnx is the source converted to fp16 (see "The fp16 conversion"). faceage-dino-vitl16-fused-fp16.onnx is the same network and weights rebuilt with fused operators, 2.6 to 3.9 times as fast on ONNX Runtime's CUDA provider (see "The fused rebuild"). Both take the same input and return the same outputs.

The source it was converted from, as downloaded:

file bytes sha256
faceage_dino_fp32.onnx 1219158136 b0fefb4473978dae8128803d4577de2586f974198991f2c6252a5609dffb3233

source/ holds the source repo's config.json, preprocessor_config.json, modeling_faceage_dino.py and configuration_faceage_dino.py, which are where the preprocessing and the decoding below come from.

Model I/O

The input face is Bx3x224x224 fp32, the batch dimension dynamic: one face crop per row, RGB in NCHW, resized to 224x224 with bicubic resampling and no aspect padding, scaled to 0..1, then normalized with ImageNet's mean (0.485, 0.456, 0.406) and standard deviation (0.229, 0.224, 0.225).

Crop the face with 10 percent proportional padding on each side: for a box x0, y0, x1, y1, widen it by 0.1 * (x1 - x0) left and right and by 0.1 * (y1 - y0) above and below, clamped to the image. That is how the model was trained, and the source card reports that without the padding its error grows from 3.56 to 3.76 years.

Two outputs, both fp32:

  • age_logits, Bx100: the CORAL thresholds. The age in years is the sum of their sigmoids, age = sum(sigmoid(age_logits[k]) for k in 0..99), a continuous value in 0..100.
  • gender_logits, Bx2: softmax, then argmax; 0 is female, 1 is male.

The source card reports MAE 3.56 years on LAGENDA (84k faces), with 1.55 on ages 0 to 12 and 2.85 on 13 to 17.

The fp16 conversion

The file is the source graph with every MatMul weight in fp16 and the input and outputs left fp32, so a caller feeds and reads fp32 tensors exactly as it would the source. The weights in fp16 are 99.5 percent of the 304M parameters, which is why the file is half the size.

A plain onnxconverter_common.float16.convert_float_to_float16 with keep_io_types=True produces a graph that returns NaN for every input. DINOv3 carries a "massive activation" on its residual stream, one channel at about 1.57e5 from the first block onward, and fp16 tops out at 65,504. Keeping the residual ops in fp32 through op_block_list is not enough on its own, because the converter wraps every blocked node with its own down-cast and up-cast, so the value still passes through fp16 between two adjacent fp32 nodes and overflows there.

So the conversion is two steps, both in scripts/:

  1. to_fp16.py LayerNormalization Softmax Add Mul converts with those four ops added to the converter's default block list.
  2. fuse_casts.py removes every Cast(to fp16) feeding a Cast(to fp32), rewiring the consumer to the original fp32 tensor, and drops the down-casts left without a reader. On this graph that removes 118 up-casts and 47 orphaned down-casts. The residual stream, all 50 LayerNormalizations and all 24 Softmaxes are then fp32 end to end, and the 192 MatMuls stay fp16.

scripts/validate_fp16.py compares the result against the source on 200 WIDER FACE validation face crops of at least 32 px, padded and resized as above:

mean absolute age difference 0.012 years
max absolute age difference 0.074 years
NaN or inf in either output none
gender agreement 200 of 200

Converted with onnx 1.22.0, onnxconverter-common 1.16.0 and onnxruntime 1.29.0 under Python 3.11; opset 18, IR 10, a single self-contained file with no external data.

The fused rebuild

faceage-dino-vitl16-fused-fp16.onnx is written by scripts/rebuild_fused.py, which reads the weights out of the source fp32 file and writes a new graph for them by hand. The network is the source's; only the way the graph is written changes:

  • The residual stream is carried scaled by 1/8, and each LayerNorm's epsilon by 1/64, so every LayerNorm's output is what it was. The massive activation on channel 757 then peaks at about 1.95e4 instead of 1.56e5, inside fp16's 65,504, and the residual stream can no longer overflow. The layer scales, times 1/8, are folded into the output projections' weights and biases.
  • Attention is one com.microsoft MultiHeadAttention per block, in fp16. The rotary position embedding is com.microsoft RotaryEmbedding, with its cos and sin tables computed once in float64 from the source's angle table and stored as fp16 constants; the CLS token and the four register tokens get rows of cos 1 and sin 0, so one operator covers all 201 tokens. Each residual add and the LayerNorm after it are one com.microsoft SkipLayerNormalization in fp32, and the MLP's bias and GELU are one com.microsoft BiasGelu.
  • v's bias is folded into o's bias. A row of attention weights sums to one, so a constant added to every value passes through attention unchanged.
  • The last block computes the query and the MLP for the CLS token only, since only the CLS token is read after it.
  • The MatMuls are fp16; the residual stream, the LayerNorms, the input and the outputs are fp32. Every Reshape takes a constant shape with the batch as -1, so no shape arithmetic runs on the host. The graph has 444 nodes, 96 of them Casts.

Compared with the source fp32 on the same 200 WIDER FACE crops as above:

mean absolute age difference, CPU provider 0.0036 years
mean absolute age difference, CUDA provider 0.0086 years

On an RTX 4090 with ONNX Runtime 1.22's CUDA provider, a batch of 1, 5 and 10 faces takes 3.4, 6.8 and 12.1 ms, against 13.2, 19.8 and 31.6 ms for faceage-dino-fp16.onnx.

The com.microsoft operators need ONNX Runtime 1.22 or later. The file uses opset 18 and com.microsoft opset 1, IR 10, in a single file with no external data. faceage-dino-fp16.onnx stays in this repo, unchanged, for the model pins that name it.

License

The weights are Apache-2.0 as the source declares. The backbone is DINOv3, and the DINOv3 License applies to it and to this derivative: use, modification and redistribution are permitted, commercial use included, on condition that the license accompanies any redistribution and that "Built with DINOv3" is displayed where the model is used. The source's training data is its own and is not distributed here.