[{"data":1,"prerenderedAt":2567},["ShallowReactive",2],{"post-\u002Fblog\u002Fbuilding-ml-inference-part-4":3},{"id":4,"title":5,"body":6,"book":2556,"date":2557,"description":2558,"extension":2559,"meta":2560,"navigation":304,"path":2561,"seo":2562,"stem":2563,"tags":2564,"__hash__":2566},"blog\u002Fblog\u002Fbuilding-ml-inference-part-4.md","Building an ML Inference API, Part IV",{"type":7,"value":8,"toc":2531},"minimark",[9,14,24,27,30,46,54,57,81,84,88,91,105,108,134,137,214,220,223,227,230,238,241,244,378,385,388,392,395,401,404,499,502,570,573,577,580,586,589,710,713,716,720,723,913,916,922,925,929,932,1002,1005,1021,1024,1438,1441,1531,1534,1538,1543,1546,1550,1553,1564,1568,1571,1575,1578,1582,1585,1596,1599,1605,1608,1611,1625,1628,1631,1635,1638,1652,1655,1827,1830,1956,1959,1963,1966,1983,1986,1990,1994,1997,2003,2070,2073,2077,2080,2086,2089,2184,2187,2304,2307,2311,2314,2386,2390,2393,2399,2417,2426,2454,2460,2470,2476,2482,2488,2494,2502,2505,2511,2514,2518,2527],[10,11,13],"h2",{"id":12},"background","Background",[15,16,17,18,23],"p",{},"By the time the FastAPI inference service from ",[19,20,22],"a",{"href":21},"\u002Fblog\u002Fbuilding-ml-inference-part-2","Part II"," was in production, the .NET side around it had all the usual machinery: HTTP clients, retries, timeouts, circuit breakers, tracing. All necessary, but still plumbing. And in some downstream systems, even a clean network call was too expensive.",[15,25,26],{},"Most of what we were serving was LightGBM regression. Training, feature engineering, and retraining schedules all belonged in Python. Inference itself was just trees, which means comparisons and additions. There's no reason that has to sit behind another network call.",[15,28,29],{},"So I started experimenting with translating trained LightGBM models directly into C#. Then callers could invoke a method instead of making an HTTP request, and the runtime path lost a few things:",[31,32,33,37,40,43],"ul",{},[34,35,36],"li",{},"the network hop",[34,38,39],{},"retry logic around inference",[34,41,42],{},"the Python process",[34,44,45],{},"a separate service to scale",[15,47,48,49,53],{},"For regression, the mapping from tree structure to code is almost literal. You generate C# source from the model, compile it into a DLL, and do inference natively inside .NET with no model file at runtime. The catch is that giant generated ",[50,51,52],"code",{},"if\u002Felse"," trees get ugly fast, and at some point it's cleaner to stop generating so much branching and represent the trees as data instead, then walk them with a small runtime. That ended up being the approach I preferred.",[15,55,56],{},"This post walks through both:",[58,59,60,63,66,69,72,75,78],"ol",{},[34,61,62],{},"Why trees can be represented directly as code",[34,64,65],{},"How LightGBM trees map to C#",[34,67,68],{},"A tiny concrete example",[34,70,71],{},"Generating the if\u002Felse version",[34,73,74],{},"Where that starts to break down",[34,76,77],{},"An evaluator that treats the model as data",[34,79,80],{},"Extending the same idea to classification",[15,82,83],{},"Regression first, because it makes the idea easiest to see.",[10,85,87],{"id":86},"a-regression-tree-is-already-code","A regression tree is already code",[15,89,90],{},"One mental shift helps a lot:",[15,92,93,94,96,97,101,102,104],{},"A decision tree is not really something you translate into ",[50,95,52],{},". It already ",[98,99,100],"em",{},"is"," ",[50,103,52],{},".",[15,106,107],{},"Take a toy tree:",[31,109,110,124],{},[34,111,112,113,116],{},"If ",[50,114,115],{},"strength_diff \u003C= -2.11",[31,117,118],{},[34,119,120,121],{},"predict ",[50,122,123],{},"0.3145",[34,125,126,127],{},"Else\n",[31,128,129],{},[34,130,120,131],{},[50,132,133],{},"0.7237",[15,135,136],{},"That is literally:",[138,139,144],"pre",{"className":140,"code":141,"language":142,"meta":143,"style":143},"language-csharp shiki shiki-themes one-light one-dark-pro","if (features[5] \u003C= -2.11)\n    return 0.3145;\nelse\n    return 0.7237;\n","csharp","",[50,145,146,186,198,204],{"__ignoreMap":143},[147,148,151,155,159,163,166,170,173,177,180,183],"span",{"class":149,"line":150},"line",1,[147,152,154],{"class":153},"sLKXg","if",[147,156,158],{"class":157},"s5ixo"," (",[147,160,162],{"class":161},"s7GmK","features",[147,164,165],{"class":157},"[",[147,167,169],{"class":168},"sAGMh","5",[147,171,172],{"class":157},"] ",[147,174,176],{"class":175},"sknuh","\u003C=",[147,178,179],{"class":175}," -",[147,181,182],{"class":168},"2.11",[147,184,185],{"class":157},")\n",[147,187,189,192,195],{"class":149,"line":188},2,[147,190,191],{"class":153},"    return",[147,193,194],{"class":168}," 0.3145",[147,196,197],{"class":157},";\n",[147,199,201],{"class":149,"line":200},3,[147,202,203],{"class":153},"else\n",[147,205,207,209,212],{"class":149,"line":206},4,[147,208,191],{"class":153},[147,210,211],{"class":168}," 0.7237",[147,213,197],{"class":157},[15,215,216,217,219],{},"That is not an approximation. That ",[98,218,100],{}," the model.",[15,221,222],{},"And that’s why tree models are unusually portable compared with a lot of other ML models.",[10,224,226],{"id":225},"boosting-is-just-many-trees-adding-corrections","Boosting is just many trees adding corrections",[15,228,229],{},"A LightGBM regression model is an ensemble:",[138,231,236],{"className":232,"code":234,"language":235,"meta":143},[233],"language-text","prediction =\n    tree1(x)\n  + tree2(x)\n  + tree3(x)\n  + ...\n","text",[50,237,234],{"__ignoreMap":143},[15,239,240],{},"Each tree contributes a little correction.",[15,242,243],{},"In code:",[138,245,247],{"className":140,"code":246,"language":142,"meta":143,"style":143},"public static double Predict(double[] f)\n{\n    double score = 0;\n\n    score += Tree0(f);\n    score += Tree1(f);\n    score += Tree2(f);\n\n    return score;\n}\n",[50,248,249,278,283,300,306,326,342,358,363,372],{"__ignoreMap":143},[147,250,251,254,257,260,264,267,270,273,276],{"class":149,"line":150},[147,252,253],{"class":153},"public",[147,255,256],{"class":153}," static",[147,258,259],{"class":153}," double",[147,261,263],{"class":262},"sAdtL"," Predict",[147,265,266],{"class":157},"(",[147,268,269],{"class":153},"double",[147,271,272],{"class":157},"[] ",[147,274,275],{"class":161},"f",[147,277,185],{"class":157},[147,279,280],{"class":149,"line":188},[147,281,282],{"class":157},"{\n",[147,284,285,288,292,295,298],{"class":149,"line":200},[147,286,287],{"class":153},"    double",[147,289,291],{"class":290},"sz0mV"," score",[147,293,294],{"class":175}," =",[147,296,297],{"class":168}," 0",[147,299,197],{"class":157},[147,301,302],{"class":149,"line":206},[147,303,305],{"emptyLinePlaceholder":304},true,"\n",[147,307,309,312,316,319,321,323],{"class":149,"line":308},5,[147,310,311],{"class":290},"    score",[147,313,315],{"class":314},"sblXP"," +=",[147,317,318],{"class":262}," Tree0",[147,320,266],{"class":157},[147,322,275],{"class":290},[147,324,325],{"class":157},");\n",[147,327,329,331,333,336,338,340],{"class":149,"line":328},6,[147,330,311],{"class":290},[147,332,315],{"class":314},[147,334,335],{"class":262}," Tree1",[147,337,266],{"class":157},[147,339,275],{"class":290},[147,341,325],{"class":157},[147,343,345,347,349,352,354,356],{"class":149,"line":344},7,[147,346,311],{"class":290},[147,348,315],{"class":314},[147,350,351],{"class":262}," Tree2",[147,353,266],{"class":157},[147,355,275],{"class":290},[147,357,325],{"class":157},[147,359,361],{"class":149,"line":360},8,[147,362,305],{"emptyLinePlaceholder":304},[147,364,366,368,370],{"class":149,"line":365},9,[147,367,191],{"class":153},[147,369,291],{"class":290},[147,371,197],{"class":157},[147,373,375],{"class":149,"line":374},10,[147,376,377],{"class":157},"}\n",[15,379,380,381,384],{},"Each ",[50,382,383],{},"TreeN"," is nested branching logic.",[15,386,387],{},"That is the whole inference loop for regression.",[10,389,391],{"id":390},"reading-a-lightgbm-tree","Reading a LightGBM tree",[15,393,394],{},"A tree dump might look something like:",[138,396,399],{"className":397,"code":398,"language":235,"meta":143},[233],"split_feature: strength_diff\nthreshold: -1.18\n\nleft_child:\n   split_feature: minute\n   threshold: 15\n   left_child: leaf=0.48\n   right_child: leaf=0.62\n\nright_child:\n   leaf=1.03\n",[50,400,398],{"__ignoreMap":143},[15,402,403],{},"Which maps directly to:",[138,405,407],{"className":140,"code":406,"language":142,"meta":143,"style":143},"if (strengthDiff \u003C= -1.18)\n{\n    if (minute \u003C= 15)\n        return 0.48;\n    else\n        return 0.62;\n}\nelse\n{\n    return 1.03;\n}\n",[50,408,409,428,432,449,459,464,473,477,481,485,494],{"__ignoreMap":143},[147,410,411,413,415,418,421,423,426],{"class":149,"line":150},[147,412,154],{"class":153},[147,414,158],{"class":157},[147,416,417],{"class":290},"strengthDiff",[147,419,420],{"class":175}," \u003C=",[147,422,179],{"class":175},[147,424,425],{"class":168},"1.18",[147,427,185],{"class":157},[147,429,430],{"class":149,"line":188},[147,431,282],{"class":157},[147,433,434,437,439,442,444,447],{"class":149,"line":200},[147,435,436],{"class":153},"    if",[147,438,158],{"class":157},[147,440,441],{"class":290},"minute",[147,443,420],{"class":175},[147,445,446],{"class":168}," 15",[147,448,185],{"class":157},[147,450,451,454,457],{"class":149,"line":206},[147,452,453],{"class":153},"        return",[147,455,456],{"class":168}," 0.48",[147,458,197],{"class":157},[147,460,461],{"class":149,"line":308},[147,462,463],{"class":153},"    else\n",[147,465,466,468,471],{"class":149,"line":328},[147,467,453],{"class":153},[147,469,470],{"class":168}," 0.62",[147,472,197],{"class":157},[147,474,475],{"class":149,"line":344},[147,476,377],{"class":157},[147,478,479],{"class":149,"line":360},[147,480,203],{"class":153},[147,482,483],{"class":149,"line":365},[147,484,282],{"class":157},[147,486,487,489,492],{"class":149,"line":374},[147,488,191],{"class":153},[147,490,491],{"class":168}," 1.03",[147,493,197],{"class":157},[147,495,497],{"class":149,"line":496},11,[147,498,377],{"class":157},[15,500,501],{},"Pretty much one to one:",[503,504,505,518],"table",{},[506,507,508],"thead",{},[509,510,511,515],"tr",{},[512,513,514],"th",{},"LightGBM",[512,516,517],{},"C#",[519,520,521,530,538,546,554,562],"tbody",{},[509,522,523,527],{},[524,525,526],"td",{},"split_feature",[524,528,529],{},"feature index",[509,531,532,535],{},[524,533,534],{},"threshold",[524,536,537],{},"comparison",[509,539,540,543],{},[524,541,542],{},"left child",[524,544,545],{},"if branch",[509,547,548,551],{},[524,549,550],{},"right child",[524,552,553],{},"else branch",[509,555,556,559],{},[524,557,558],{},"leaf value",[524,560,561],{},"returned value",[509,563,564,567],{},[524,565,566],{},"ensemble",[524,568,569],{},"sum of trees",[15,571,572],{},"Once that clicks, everything else follows naturally.",[10,574,576],{"id":575},"tiny-example","Tiny example",[15,578,579],{},"Suppose a model has:",[138,581,584],{"className":582,"code":583,"language":235,"meta":143},[233],"Tree=0\nnum_leaves=3\nsplit_feature=5 7\nthreshold=-1.18 15\nleft_child=1 -1\nright_child=2 -2\nleaf_value=0.48 0.62 1.03\n",[50,585,583],{"__ignoreMap":143},[15,587,588],{},"Generated C#:",[138,590,592],{"className":140,"code":591,"language":142,"meta":143,"style":143},"static double Tree0(double[] f)\n{\n    if (f[5] \u003C= -1.18)\n    {\n        if (f[7] \u003C= 15)\n            return 0.48;\n        else\n            return 0.62;\n    }\n\n    return 1.03;\n}\n",[50,593,594,613,617,639,644,666,675,680,688,693,697,705],{"__ignoreMap":143},[147,595,596,599,601,603,605,607,609,611],{"class":149,"line":150},[147,597,598],{"class":153},"static",[147,600,259],{"class":153},[147,602,318],{"class":262},[147,604,266],{"class":157},[147,606,269],{"class":153},[147,608,272],{"class":157},[147,610,275],{"class":161},[147,612,185],{"class":157},[147,614,615],{"class":149,"line":188},[147,616,282],{"class":157},[147,618,619,621,623,625,627,629,631,633,635,637],{"class":149,"line":200},[147,620,436],{"class":153},[147,622,158],{"class":157},[147,624,275],{"class":161},[147,626,165],{"class":157},[147,628,169],{"class":168},[147,630,172],{"class":157},[147,632,176],{"class":175},[147,634,179],{"class":175},[147,636,425],{"class":168},[147,638,185],{"class":157},[147,640,641],{"class":149,"line":206},[147,642,643],{"class":157},"    {\n",[147,645,646,649,651,653,655,658,660,662,664],{"class":149,"line":308},[147,647,648],{"class":153},"        if",[147,650,158],{"class":157},[147,652,275],{"class":161},[147,654,165],{"class":157},[147,656,657],{"class":168},"7",[147,659,172],{"class":157},[147,661,176],{"class":175},[147,663,446],{"class":168},[147,665,185],{"class":157},[147,667,668,671,673],{"class":149,"line":328},[147,669,670],{"class":153},"            return",[147,672,456],{"class":168},[147,674,197],{"class":157},[147,676,677],{"class":149,"line":344},[147,678,679],{"class":153},"        else\n",[147,681,682,684,686],{"class":149,"line":360},[147,683,670],{"class":153},[147,685,470],{"class":168},[147,687,197],{"class":157},[147,689,690],{"class":149,"line":365},[147,691,692],{"class":157},"    }\n",[147,694,695],{"class":149,"line":374},[147,696,305],{"emptyLinePlaceholder":304},[147,698,699,701,703],{"class":149,"line":496},[147,700,191],{"class":153},[147,702,491],{"class":168},[147,704,197],{"class":157},[147,706,708],{"class":149,"line":707},12,[147,709,377],{"class":157},[15,711,712],{},"That is the tree.",[15,714,715],{},"If you have 300 trees, generate 300 methods. And this is surprisingly workable.",[10,717,719],{"id":718},"verifying-inference","Verifying inference",[15,721,722],{},"Using one row from my model:",[138,724,726],{"className":140,"code":725,"language":142,"meta":143,"style":143},"var result = Model.Predict([\n1.2699999809265137,\n3.380000114440918,\n0.7300000190734863,\n1.6299999952316284,\n4.650000095367432,\n-2.1100001335144043,\n1.0,\n28.0,\n0.0,\n0.0,\n2.0,\n2.0,\n-2.0,\n0.0,\n2.0,\n2.0,\n-2.0,\n7.0,\n-0.40880000591278076,\n1.0871999263763428,\n427465.0\n]);\n",[50,727,728,749,757,764,771,778,785,795,802,809,816,822,829,836,845,852,859,866,875,883,893,901,907],{"__ignoreMap":143},[147,729,730,733,736,738,741,743,746],{"class":149,"line":150},[147,731,732],{"class":153},"var",[147,734,735],{"class":290}," result",[147,737,294],{"class":175},[147,739,740],{"class":161}," Model",[147,742,104],{"class":157},[147,744,745],{"class":262},"Predict",[147,747,748],{"class":157},"([\n",[147,750,751,754],{"class":149,"line":188},[147,752,753],{"class":168},"1.2699999809265137",[147,755,756],{"class":157},",\n",[147,758,759,762],{"class":149,"line":200},[147,760,761],{"class":168},"3.380000114440918",[147,763,756],{"class":157},[147,765,766,769],{"class":149,"line":206},[147,767,768],{"class":168},"0.7300000190734863",[147,770,756],{"class":157},[147,772,773,776],{"class":149,"line":308},[147,774,775],{"class":168},"1.6299999952316284",[147,777,756],{"class":157},[147,779,780,783],{"class":149,"line":328},[147,781,782],{"class":168},"4.650000095367432",[147,784,756],{"class":157},[147,786,787,790,793],{"class":149,"line":344},[147,788,789],{"class":175},"-",[147,791,792],{"class":168},"2.1100001335144043",[147,794,756],{"class":157},[147,796,797,800],{"class":149,"line":360},[147,798,799],{"class":168},"1.0",[147,801,756],{"class":157},[147,803,804,807],{"class":149,"line":365},[147,805,806],{"class":168},"28.0",[147,808,756],{"class":157},[147,810,811,814],{"class":149,"line":374},[147,812,813],{"class":168},"0.0",[147,815,756],{"class":157},[147,817,818,820],{"class":149,"line":496},[147,819,813],{"class":168},[147,821,756],{"class":157},[147,823,824,827],{"class":149,"line":707},[147,825,826],{"class":168},"2.0",[147,828,756],{"class":157},[147,830,832,834],{"class":149,"line":831},13,[147,833,826],{"class":168},[147,835,756],{"class":157},[147,837,839,841,843],{"class":149,"line":838},14,[147,840,789],{"class":175},[147,842,826],{"class":168},[147,844,756],{"class":157},[147,846,848,850],{"class":149,"line":847},15,[147,849,813],{"class":168},[147,851,756],{"class":157},[147,853,855,857],{"class":149,"line":854},16,[147,856,826],{"class":168},[147,858,756],{"class":157},[147,860,862,864],{"class":149,"line":861},17,[147,863,826],{"class":168},[147,865,756],{"class":157},[147,867,869,871,873],{"class":149,"line":868},18,[147,870,789],{"class":175},[147,872,826],{"class":168},[147,874,756],{"class":157},[147,876,878,881],{"class":149,"line":877},19,[147,879,880],{"class":168},"7.0",[147,882,756],{"class":157},[147,884,886,888,891],{"class":149,"line":885},20,[147,887,789],{"class":175},[147,889,890],{"class":168},"0.40880000591278076",[147,892,756],{"class":157},[147,894,896,899],{"class":149,"line":895},21,[147,897,898],{"class":168},"1.0871999263763428",[147,900,756],{"class":157},[147,902,904],{"class":149,"line":903},22,[147,905,906],{"class":168},"427465.0\n",[147,908,910],{"class":149,"line":909},23,[147,911,912],{"class":157},"]);\n",[15,914,915],{},"Expected:",[138,917,920],{"className":918,"code":919,"language":235,"meta":143},[233],"0.31458735\n",[50,921,919],{"__ignoreMap":143},[15,923,924],{},"Generated predictor matched exactly.",[10,926,928],{"id":927},"generating-the-c","Generating the C#",[15,930,931],{},"This should absolutely be generated. Do not hand write trees. For any real model, it gets impossible very quickly. I used Python to generate the C# code, but the language of the exporter does not matter much.",[138,933,937],{"className":934,"code":935,"language":936,"meta":143,"style":143},"language-python shiki shiki-themes one-light one-dark-pro","import lightgbm as lgb\n\nbooster = lgb.Booster(model_file=\"model.txt\")\nmodel_dump = booster.dump_model()\n","python",[50,938,939,953,957,986],{"__ignoreMap":143},[147,940,941,944,947,950],{"class":149,"line":150},[147,942,943],{"class":153},"import",[147,945,946],{"class":157}," lightgbm ",[147,948,949],{"class":153},"as",[147,951,952],{"class":157}," lgb\n",[147,954,955],{"class":149,"line":188},[147,956,305],{"emptyLinePlaceholder":304},[147,958,959,962,965,968,972,974,978,980,984],{"class":149,"line":200},[147,960,961],{"class":157},"booster ",[147,963,964],{"class":175},"=",[147,966,967],{"class":157}," lgb.",[147,969,971],{"class":970},"slOjB","Booster",[147,973,266],{"class":157},[147,975,977],{"class":976},"sp7wS","model_file",[147,979,964],{"class":175},[147,981,983],{"class":982},"sDhpE","\"model.txt\"",[147,985,185],{"class":157},[147,987,988,991,993,996,999],{"class":149,"line":206},[147,989,990],{"class":157},"model_dump ",[147,992,964],{"class":175},[147,994,995],{"class":157}," booster.",[147,997,998],{"class":970},"dump_model",[147,1000,1001],{"class":157},"()\n",[15,1003,1004],{},"Trees live in:",[138,1006,1008],{"className":934,"code":1007,"language":936,"meta":143,"style":143},"model_dump[\"tree_info\"]\n",[50,1009,1010],{"__ignoreMap":143},[147,1011,1012,1015,1018],{"class":149,"line":150},[147,1013,1014],{"class":157},"model_dump[",[147,1016,1017],{"class":982},"\"tree_info\"",[147,1019,1020],{"class":157},"]\n",[15,1022,1023],{},"Recursive emitter:",[138,1025,1027],{"className":934,"code":1026,"language":936,"meta":143,"style":143},"def emit_node(node, indent=1):\n    pad = \"    \" * indent\n\n    if \"leaf_value\" in node:\n        return f\"{pad}return {node['leaf_value']};\\n\"\n\n    feature = node[\"split_feature\"]\n    threshold = node[\"threshold\"]\n\n    code = []\n\n    code.append(\n        f\"{pad}if (features[{feature}] \u003C= {threshold})\\n\"\n        f\"{pad}{{\\n\"\n    )\n\n    code.append(emit_node(node[\"left_child\"], indent+1))\n\n    code.append(\n        f\"{pad}}}\\n\"\n        f\"{pad}else\\n\"\n        f\"{pad}{{\\n\"\n    )\n\n    code.append(emit_node(node[\"right_child\"], indent+1))\n\n    code.append(f\"{pad}}}\\n\")\n\n    return \"\".join(code)\n",[50,1028,1029,1057,1073,1077,1090,1135,1139,1154,1168,1172,1182,1186,1197,1236,1253,1258,1262,1290,1294,1302,1319,1338,1354,1358,1363,1387,1392,1417,1422],{"__ignoreMap":143},[147,1030,1031,1034,1037,1039,1043,1046,1049,1051,1054],{"class":149,"line":150},[147,1032,1033],{"class":153},"def",[147,1035,1036],{"class":262}," emit_node",[147,1038,266],{"class":157},[147,1040,1042],{"class":1041},"so_Uh","node",[147,1044,1045],{"class":157},",",[147,1047,1048],{"class":1041}," indent",[147,1050,964],{"class":157},[147,1052,1053],{"class":168},"1",[147,1055,1056],{"class":157},"):\n",[147,1058,1059,1062,1064,1067,1070],{"class":149,"line":188},[147,1060,1061],{"class":157},"    pad ",[147,1063,964],{"class":175},[147,1065,1066],{"class":982}," \"    \"",[147,1068,1069],{"class":175}," *",[147,1071,1072],{"class":157}," indent\n",[147,1074,1075],{"class":149,"line":200},[147,1076,305],{"emptyLinePlaceholder":304},[147,1078,1079,1081,1084,1087],{"class":149,"line":206},[147,1080,436],{"class":153},[147,1082,1083],{"class":982}," \"leaf_value\"",[147,1085,1086],{"class":153}," in",[147,1088,1089],{"class":157}," node:\n",[147,1091,1092,1094,1097,1100,1103,1106,1109,1112,1114,1117,1120,1123,1125,1128,1132],{"class":149,"line":308},[147,1093,453],{"class":153},[147,1095,1096],{"class":153}," f",[147,1098,1099],{"class":982},"\"",[147,1101,1102],{"class":168},"{",[147,1104,1105],{"class":157},"pad",[147,1107,1108],{"class":168},"}",[147,1110,1111],{"class":982},"return ",[147,1113,1102],{"class":168},[147,1115,1116],{"class":157},"node[",[147,1118,1119],{"class":982},"'leaf_value'",[147,1121,1122],{"class":157},"]",[147,1124,1108],{"class":168},[147,1126,1127],{"class":982},";",[147,1129,1131],{"class":1130},"s_Sar","\\n",[147,1133,1134],{"class":982},"\"\n",[147,1136,1137],{"class":149,"line":328},[147,1138,305],{"emptyLinePlaceholder":304},[147,1140,1141,1144,1146,1149,1152],{"class":149,"line":344},[147,1142,1143],{"class":157},"    feature ",[147,1145,964],{"class":175},[147,1147,1148],{"class":157}," node[",[147,1150,1151],{"class":982},"\"split_feature\"",[147,1153,1020],{"class":157},[147,1155,1156,1159,1161,1163,1166],{"class":149,"line":360},[147,1157,1158],{"class":157},"    threshold ",[147,1160,964],{"class":175},[147,1162,1148],{"class":157},[147,1164,1165],{"class":982},"\"threshold\"",[147,1167,1020],{"class":157},[147,1169,1170],{"class":149,"line":365},[147,1171,305],{"emptyLinePlaceholder":304},[147,1173,1174,1177,1179],{"class":149,"line":374},[147,1175,1176],{"class":157},"    code ",[147,1178,964],{"class":175},[147,1180,1181],{"class":157}," []\n",[147,1183,1184],{"class":149,"line":496},[147,1185,305],{"emptyLinePlaceholder":304},[147,1187,1188,1191,1194],{"class":149,"line":707},[147,1189,1190],{"class":157},"    code.",[147,1192,1193],{"class":970},"append",[147,1195,1196],{"class":157},"(\n",[147,1198,1199,1202,1204,1206,1208,1210,1213,1215,1218,1220,1223,1225,1227,1229,1232,1234],{"class":149,"line":831},[147,1200,1201],{"class":153},"        f",[147,1203,1099],{"class":982},[147,1205,1102],{"class":168},[147,1207,1105],{"class":157},[147,1209,1108],{"class":168},[147,1211,1212],{"class":982},"if (features[",[147,1214,1102],{"class":168},[147,1216,1217],{"class":157},"feature",[147,1219,1108],{"class":168},[147,1221,1222],{"class":982},"] \u003C= ",[147,1224,1102],{"class":168},[147,1226,534],{"class":157},[147,1228,1108],{"class":168},[147,1230,1231],{"class":982},")",[147,1233,1131],{"class":1130},[147,1235,1134],{"class":982},[147,1237,1238,1240,1242,1244,1246,1248,1251],{"class":149,"line":838},[147,1239,1201],{"class":153},[147,1241,1099],{"class":982},[147,1243,1102],{"class":168},[147,1245,1105],{"class":157},[147,1247,1108],{"class":168},[147,1249,1250],{"class":1130},"{{\\n",[147,1252,1134],{"class":982},[147,1254,1255],{"class":149,"line":847},[147,1256,1257],{"class":157},"    )\n",[147,1259,1260],{"class":149,"line":854},[147,1261,305],{"emptyLinePlaceholder":304},[147,1263,1264,1266,1268,1270,1273,1276,1279,1282,1285,1287],{"class":149,"line":861},[147,1265,1190],{"class":157},[147,1267,1193],{"class":970},[147,1269,266],{"class":157},[147,1271,1272],{"class":970},"emit_node",[147,1274,1275],{"class":157},"(node[",[147,1277,1278],{"class":982},"\"left_child\"",[147,1280,1281],{"class":157},"], indent",[147,1283,1284],{"class":175},"+",[147,1286,1053],{"class":168},[147,1288,1289],{"class":157},"))\n",[147,1291,1292],{"class":149,"line":868},[147,1293,305],{"emptyLinePlaceholder":304},[147,1295,1296,1298,1300],{"class":149,"line":877},[147,1297,1190],{"class":157},[147,1299,1193],{"class":970},[147,1301,1196],{"class":157},[147,1303,1304,1306,1308,1310,1312,1314,1317],{"class":149,"line":885},[147,1305,1201],{"class":153},[147,1307,1099],{"class":982},[147,1309,1102],{"class":168},[147,1311,1105],{"class":157},[147,1313,1108],{"class":168},[147,1315,1316],{"class":1130},"}}\\n",[147,1318,1134],{"class":982},[147,1320,1321,1323,1325,1327,1329,1331,1334,1336],{"class":149,"line":895},[147,1322,1201],{"class":153},[147,1324,1099],{"class":982},[147,1326,1102],{"class":168},[147,1328,1105],{"class":157},[147,1330,1108],{"class":168},[147,1332,1333],{"class":982},"else",[147,1335,1131],{"class":1130},[147,1337,1134],{"class":982},[147,1339,1340,1342,1344,1346,1348,1350,1352],{"class":149,"line":903},[147,1341,1201],{"class":153},[147,1343,1099],{"class":982},[147,1345,1102],{"class":168},[147,1347,1105],{"class":157},[147,1349,1108],{"class":168},[147,1351,1250],{"class":1130},[147,1353,1134],{"class":982},[147,1355,1356],{"class":149,"line":909},[147,1357,1257],{"class":157},[147,1359,1361],{"class":149,"line":1360},24,[147,1362,305],{"emptyLinePlaceholder":304},[147,1364,1366,1368,1370,1372,1374,1376,1379,1381,1383,1385],{"class":149,"line":1365},25,[147,1367,1190],{"class":157},[147,1369,1193],{"class":970},[147,1371,266],{"class":157},[147,1373,1272],{"class":970},[147,1375,1275],{"class":157},[147,1377,1378],{"class":982},"\"right_child\"",[147,1380,1281],{"class":157},[147,1382,1284],{"class":175},[147,1384,1053],{"class":168},[147,1386,1289],{"class":157},[147,1388,1390],{"class":149,"line":1389},26,[147,1391,305],{"emptyLinePlaceholder":304},[147,1393,1395,1397,1399,1401,1403,1405,1407,1409,1411,1413,1415],{"class":149,"line":1394},27,[147,1396,1190],{"class":157},[147,1398,1193],{"class":970},[147,1400,266],{"class":157},[147,1402,275],{"class":153},[147,1404,1099],{"class":982},[147,1406,1102],{"class":168},[147,1408,1105],{"class":157},[147,1410,1108],{"class":168},[147,1412,1316],{"class":1130},[147,1414,1099],{"class":982},[147,1416,185],{"class":157},[147,1418,1420],{"class":149,"line":1419},28,[147,1421,305],{"emptyLinePlaceholder":304},[147,1423,1425,1427,1430,1432,1435],{"class":149,"line":1424},29,[147,1426,191],{"class":153},[147,1428,1429],{"class":982}," \"\"",[147,1431,104],{"class":157},[147,1433,1434],{"class":970},"join",[147,1436,1437],{"class":157},"(code)\n",[15,1439,1440],{},"Generate methods:",[138,1442,1444],{"className":934,"code":1443,"language":936,"meta":143,"style":143},"for i, tree in enumerate(model_dump[\"tree_info\"]):\n    print(f\"static double Tree{i}(double[] features)\")\n    print(\"{\")\n    print(emit_node(tree[\"tree_structure\"]))\n    print(\"}\")\n",[50,1445,1446,1468,1492,1503,1520],{"__ignoreMap":143},[147,1447,1448,1451,1454,1457,1460,1463,1465],{"class":149,"line":150},[147,1449,1450],{"class":153},"for",[147,1452,1453],{"class":157}," i, tree ",[147,1455,1456],{"class":153},"in",[147,1458,1459],{"class":1130}," enumerate",[147,1461,1462],{"class":157},"(model_dump[",[147,1464,1017],{"class":982},[147,1466,1467],{"class":157},"]):\n",[147,1469,1470,1473,1475,1477,1480,1482,1485,1487,1490],{"class":149,"line":188},[147,1471,1472],{"class":1130},"    print",[147,1474,266],{"class":157},[147,1476,275],{"class":153},[147,1478,1479],{"class":982},"\"static double Tree",[147,1481,1102],{"class":168},[147,1483,1484],{"class":157},"i",[147,1486,1108],{"class":168},[147,1488,1489],{"class":982},"(double[] features)\"",[147,1491,185],{"class":157},[147,1493,1494,1496,1498,1501],{"class":149,"line":200},[147,1495,1472],{"class":1130},[147,1497,266],{"class":157},[147,1499,1500],{"class":982},"\"{\"",[147,1502,185],{"class":157},[147,1504,1505,1507,1509,1511,1514,1517],{"class":149,"line":206},[147,1506,1472],{"class":1130},[147,1508,266],{"class":157},[147,1510,1272],{"class":970},[147,1512,1513],{"class":157},"(tree[",[147,1515,1516],{"class":982},"\"tree_structure\"",[147,1518,1519],{"class":157},"]))\n",[147,1521,1522,1524,1526,1529],{"class":149,"line":308},[147,1523,1472],{"class":1130},[147,1525,266],{"class":157},[147,1527,1528],{"class":982},"\"}\"",[147,1530,185],{"class":157},[15,1532,1533],{},"The basic version is quite small.",[10,1535,1537],{"id":1536},"why-i-liked-this","Why I liked this",[1539,1540,1542],"h3",{"id":1541},"native-inference","Native inference",[15,1544,1545],{},"The generated predictor does not need a Python runtime or a LightGBM dependency. It is just .NET code.",[1539,1547,1549],{"id":1548},"speed","Speed",[15,1551,1552],{},"Inference becomes a small set of cheap operations:",[31,1554,1555,1558,1561],{},[34,1556,1557],{},"comparisons",[34,1559,1560],{},"branches",[34,1562,1563],{},"additions",[1539,1565,1567],{"id":1566},"deployment","Deployment",[15,1569,1570],{},"The model becomes source. Ship a DLL and you've shipped the model.",[1539,1572,1574],{"id":1573},"debugging","Debugging",[15,1576,1577],{},"You can inspect actual decision paths, which is useful in pricing or risk sensitive systems.",[10,1579,1581],{"id":1580},"where-it-starts-breaking-down","Where it starts breaking down",[15,1583,1584],{},"Say:",[31,1586,1587,1590,1593],{},[34,1588,1589],{},"500 trees",[34,1591,1592],{},"depth 8",[34,1594,1595],{},"~255 nodes each",[15,1597,1598],{},"That’s potentially:",[138,1600,1603],{"className":1601,"code":1602,"language":235,"meta":143},[233],"127,500 node checks\n",[50,1604,1602],{"__ignoreMap":143},[15,1606,1607],{},"Now generated code gets huge.",[15,1609,1610],{},"You start getting:",[31,1612,1613,1616,1619,1622],{},[34,1614,1615],{},"giant files",[34,1617,1618],{},"ugly diffs",[34,1620,1621],{},"slower compile times",[34,1623,1624],{},"questionable JIT behavior",[15,1626,1627],{},"It still works. For example, one of my models had 150 trees and 25 features, and the generated C# was about 130k lines. If you use Rider or another IDE that parses C#, you will feel it slow down.",[15,1629,1630],{},"That pushed me toward a better representation.",[10,1632,1634],{"id":1633},"represent-the-model-as-data","Represent the model as data",[15,1636,1637],{},"Instead of generating giant branch forests, export the model the way it already exists internally:",[31,1639,1640,1643,1646,1649],{},[34,1641,1642],{},"feature arrays",[34,1644,1645],{},"thresholds",[34,1647,1648],{},"child pointers",[34,1650,1651],{},"leaf values",[15,1653,1654],{},"Then use a tiny evaluator:",[138,1656,1658],{"className":140,"code":1657,"language":142,"meta":143,"style":143},"private static double Eval(int node, ReadOnlySpan\u003Cdouble> f)\n{\n    while (true)\n    {\n        if (IsLeaf[node])\n            return Value[node];\n\n        if (f[Feature[node]] \u003C= Threshold[node])\n            node = Left[node];\n        else\n            node = Right[node];\n    }\n}\n",[50,1659,1660,1699,1703,1715,1719,1735,1749,1753,1784,1800,1804,1819,1823],{"__ignoreMap":143},[147,1661,1662,1665,1667,1669,1672,1674,1677,1680,1683,1687,1690,1692,1695,1697],{"class":149,"line":150},[147,1663,1664],{"class":153},"private",[147,1666,256],{"class":153},[147,1668,259],{"class":153},[147,1670,1671],{"class":262}," Eval",[147,1673,266],{"class":157},[147,1675,1676],{"class":153},"int",[147,1678,1679],{"class":161}," node",[147,1681,1682],{"class":157},", ",[147,1684,1686],{"class":1685},"sC09Y","ReadOnlySpan",[147,1688,1689],{"class":157},"\u003C",[147,1691,269],{"class":153},[147,1693,1694],{"class":157},"> ",[147,1696,275],{"class":161},[147,1698,185],{"class":157},[147,1700,1701],{"class":149,"line":188},[147,1702,282],{"class":157},[147,1704,1705,1708,1710,1713],{"class":149,"line":200},[147,1706,1707],{"class":153},"    while",[147,1709,158],{"class":157},[147,1711,1712],{"class":168},"true",[147,1714,185],{"class":157},[147,1716,1717],{"class":149,"line":206},[147,1718,643],{"class":157},[147,1720,1721,1723,1725,1728,1730,1732],{"class":149,"line":308},[147,1722,648],{"class":153},[147,1724,158],{"class":157},[147,1726,1727],{"class":161},"IsLeaf",[147,1729,165],{"class":157},[147,1731,1042],{"class":290},[147,1733,1734],{"class":157},"])\n",[147,1736,1737,1739,1742,1744,1746],{"class":149,"line":328},[147,1738,670],{"class":153},[147,1740,1741],{"class":161}," Value",[147,1743,165],{"class":157},[147,1745,1042],{"class":290},[147,1747,1748],{"class":157},"];\n",[147,1750,1751],{"class":149,"line":344},[147,1752,305],{"emptyLinePlaceholder":304},[147,1754,1755,1757,1759,1761,1763,1766,1768,1770,1773,1775,1778,1780,1782],{"class":149,"line":360},[147,1756,648],{"class":153},[147,1758,158],{"class":157},[147,1760,275],{"class":161},[147,1762,165],{"class":157},[147,1764,1765],{"class":161},"Feature",[147,1767,165],{"class":157},[147,1769,1042],{"class":290},[147,1771,1772],{"class":157},"]] ",[147,1774,176],{"class":175},[147,1776,1777],{"class":161}," Threshold",[147,1779,165],{"class":157},[147,1781,1042],{"class":290},[147,1783,1734],{"class":157},[147,1785,1786,1789,1791,1794,1796,1798],{"class":149,"line":365},[147,1787,1788],{"class":290},"            node",[147,1790,294],{"class":175},[147,1792,1793],{"class":161}," Left",[147,1795,165],{"class":157},[147,1797,1042],{"class":290},[147,1799,1748],{"class":157},[147,1801,1802],{"class":149,"line":374},[147,1803,679],{"class":153},[147,1805,1806,1808,1810,1813,1815,1817],{"class":149,"line":496},[147,1807,1788],{"class":290},[147,1809,294],{"class":175},[147,1811,1812],{"class":161}," Right",[147,1814,165],{"class":157},[147,1816,1042],{"class":290},[147,1818,1748],{"class":157},[147,1820,1821],{"class":149,"line":707},[147,1822,692],{"class":157},[147,1824,1825],{"class":149,"line":831},[147,1826,377],{"class":157},[15,1828,1829],{},"Boosting:",[138,1831,1833],{"className":140,"code":1832,"language":142,"meta":143,"style":143},"public static double Predict(ReadOnlySpan\u003Cdouble> f)\n{\n    double score = 0;\n\n    for (int i = 0; i \u003C TreeCount; i++)\n        score += Eval(Roots[i], f);\n\n    return score;\n}\n",[50,1834,1835,1859,1863,1875,1879,1915,1940,1944,1952],{"__ignoreMap":143},[147,1836,1837,1839,1841,1843,1845,1847,1849,1851,1853,1855,1857],{"class":149,"line":150},[147,1838,253],{"class":153},[147,1840,256],{"class":153},[147,1842,259],{"class":153},[147,1844,263],{"class":262},[147,1846,266],{"class":157},[147,1848,1686],{"class":1685},[147,1850,1689],{"class":157},[147,1852,269],{"class":153},[147,1854,1694],{"class":157},[147,1856,275],{"class":161},[147,1858,185],{"class":157},[147,1860,1861],{"class":149,"line":188},[147,1862,282],{"class":157},[147,1864,1865,1867,1869,1871,1873],{"class":149,"line":200},[147,1866,287],{"class":153},[147,1868,291],{"class":290},[147,1870,294],{"class":175},[147,1872,297],{"class":168},[147,1874,197],{"class":157},[147,1876,1877],{"class":149,"line":206},[147,1878,305],{"emptyLinePlaceholder":304},[147,1880,1881,1884,1886,1888,1891,1893,1895,1898,1900,1903,1906,1908,1910,1913],{"class":149,"line":308},[147,1882,1883],{"class":153},"    for",[147,1885,158],{"class":157},[147,1887,1676],{"class":153},[147,1889,1890],{"class":290}," i",[147,1892,294],{"class":175},[147,1894,297],{"class":168},[147,1896,1897],{"class":157},"; ",[147,1899,1484],{"class":290},[147,1901,1902],{"class":175}," \u003C",[147,1904,1905],{"class":290}," TreeCount",[147,1907,1897],{"class":157},[147,1909,1484],{"class":290},[147,1911,1912],{"class":175},"++",[147,1914,185],{"class":157},[147,1916,1917,1920,1922,1924,1926,1929,1931,1933,1936,1938],{"class":149,"line":328},[147,1918,1919],{"class":290},"        score",[147,1921,315],{"class":314},[147,1923,1671],{"class":262},[147,1925,266],{"class":157},[147,1927,1928],{"class":161},"Roots",[147,1930,165],{"class":157},[147,1932,1484],{"class":290},[147,1934,1935],{"class":157},"], ",[147,1937,275],{"class":290},[147,1939,325],{"class":157},[147,1941,1942],{"class":149,"line":344},[147,1943,305],{"emptyLinePlaceholder":304},[147,1945,1946,1948,1950],{"class":149,"line":360},[147,1947,191],{"class":153},[147,1949,291],{"class":290},[147,1951,197],{"class":157},[147,1953,1954],{"class":149,"line":365},[147,1955,377],{"class":157},[15,1957,1958],{},"Still exact inference. Just much cleaner.",[10,1960,1962],{"id":1961},"why-i-ended-up-preferring-this","Why I ended up preferring this",[15,1964,1965],{},"Compared with giant generated if\u002Felse:",[31,1967,1968,1971,1974,1977,1980],{},[34,1969,1970],{},"much smaller generated source",[34,1972,1973],{},"one evaluator method",[34,1975,1976],{},"cleaner diffs",[34,1978,1979],{},"easier codegen",[34,1981,1982],{},"friendlier for JIT",[15,1984,1985],{},"And source size grows mostly with model data, not duplicated branching syntax.",[10,1987,1989],{"id":1988},"classification-extends-naturally","Classification extends naturally",[1539,1991,1993],{"id":1992},"binary-classification","Binary classification",[15,1995,1996],{},"Same trees. Usually sum the logits, then apply sigmoid:",[138,1998,2001],{"className":1999,"code":2000,"language":235,"meta":143},[233],"probability = sigmoid(sum)\n",[50,2002,2000],{"__ignoreMap":143},[138,2004,2006],{"className":140,"code":2005,"language":142,"meta":143,"style":143},"static double Sigmoid(double x)\n{\n   return 1 \u002F (1 + Math.Exp(-x));\n}\n",[50,2007,2008,2026,2030,2066],{"__ignoreMap":143},[147,2009,2010,2012,2014,2017,2019,2021,2024],{"class":149,"line":150},[147,2011,598],{"class":153},[147,2013,259],{"class":153},[147,2015,2016],{"class":262}," Sigmoid",[147,2018,266],{"class":157},[147,2020,269],{"class":153},[147,2022,2023],{"class":161}," x",[147,2025,185],{"class":157},[147,2027,2028],{"class":149,"line":188},[147,2029,282],{"class":157},[147,2031,2032,2035,2038,2041,2043,2045,2048,2051,2053,2056,2058,2060,2063],{"class":149,"line":200},[147,2033,2034],{"class":153},"   return",[147,2036,2037],{"class":168}," 1",[147,2039,2040],{"class":175}," \u002F",[147,2042,158],{"class":157},[147,2044,1053],{"class":168},[147,2046,2047],{"class":175}," +",[147,2049,2050],{"class":161}," Math",[147,2052,104],{"class":157},[147,2054,2055],{"class":262},"Exp",[147,2057,266],{"class":157},[147,2059,789],{"class":175},[147,2061,2062],{"class":290},"x",[147,2064,2065],{"class":157},"));\n",[147,2067,2068],{"class":149,"line":206},[147,2069,377],{"class":157},[15,2071,2072],{},"Then threshold. Same traversal, different output transform.",[1539,2074,2076],{"id":2075},"multiclass","Multiclass",[15,2078,2079],{},"Often:",[138,2081,2084],{"className":2082,"code":2083,"language":235,"meta":143},[233],"num_classes × boosting_rounds trees\n",[50,2085,2083],{"__ignoreMap":143},[15,2087,2088],{},"Accumulate per class:",[138,2090,2092],{"className":140,"code":2091,"language":142,"meta":143,"style":143},"double[] scores = new double[3];\n\nscores[0] += Tree0(f);\nscores[1] += Tree1(f);\nscores[2] += Tree2(f);\n",[50,2093,2094,2117,2121,2143,2163],{"__ignoreMap":143},[147,2095,2096,2098,2100,2103,2105,2108,2110,2112,2115],{"class":149,"line":150},[147,2097,269],{"class":153},[147,2099,272],{"class":157},[147,2101,2102],{"class":290},"scores",[147,2104,294],{"class":175},[147,2106,2107],{"class":157}," new ",[147,2109,269],{"class":153},[147,2111,165],{"class":157},[147,2113,2114],{"class":168},"3",[147,2116,1748],{"class":157},[147,2118,2119],{"class":149,"line":188},[147,2120,305],{"emptyLinePlaceholder":304},[147,2122,2123,2125,2127,2130,2132,2135,2137,2139,2141],{"class":149,"line":200},[147,2124,2102],{"class":161},[147,2126,165],{"class":157},[147,2128,2129],{"class":168},"0",[147,2131,172],{"class":157},[147,2133,2134],{"class":314},"+=",[147,2136,318],{"class":262},[147,2138,266],{"class":157},[147,2140,275],{"class":290},[147,2142,325],{"class":157},[147,2144,2145,2147,2149,2151,2153,2155,2157,2159,2161],{"class":149,"line":206},[147,2146,2102],{"class":161},[147,2148,165],{"class":157},[147,2150,1053],{"class":168},[147,2152,172],{"class":157},[147,2154,2134],{"class":314},[147,2156,335],{"class":262},[147,2158,266],{"class":157},[147,2160,275],{"class":290},[147,2162,325],{"class":157},[147,2164,2165,2167,2169,2172,2174,2176,2178,2180,2182],{"class":149,"line":308},[147,2166,2102],{"class":161},[147,2168,165],{"class":157},[147,2170,2171],{"class":168},"2",[147,2173,172],{"class":157},[147,2175,2134],{"class":314},[147,2177,351],{"class":262},[147,2179,266],{"class":157},[147,2181,275],{"class":290},[147,2183,325],{"class":157},[15,2185,2186],{},"Then softmax:",[138,2188,2190],{"className":140,"code":2189,"language":142,"meta":143,"style":143},"static double[] Softmax(double[] x)\n{\n    var exp = x.Select(Math.Exp).ToArray();\n    var sum = exp.Sum();\n    return exp.Select(v => v \u002F sum).ToArray();\n}\n",[50,2191,2192,2213,2217,2252,2270,2300],{"__ignoreMap":143},[147,2193,2194,2196,2198,2200,2203,2205,2207,2209,2211],{"class":149,"line":150},[147,2195,598],{"class":153},[147,2197,259],{"class":153},[147,2199,272],{"class":157},[147,2201,2202],{"class":262},"Softmax",[147,2204,266],{"class":157},[147,2206,269],{"class":153},[147,2208,272],{"class":157},[147,2210,2062],{"class":161},[147,2212,185],{"class":157},[147,2214,2215],{"class":149,"line":188},[147,2216,282],{"class":157},[147,2218,2219,2222,2225,2227,2229,2231,2234,2236,2239,2241,2243,2246,2249],{"class":149,"line":200},[147,2220,2221],{"class":153},"    var",[147,2223,2224],{"class":290}," exp",[147,2226,294],{"class":175},[147,2228,2023],{"class":161},[147,2230,104],{"class":157},[147,2232,2233],{"class":262},"Select",[147,2235,266],{"class":157},[147,2237,2238],{"class":161},"Math",[147,2240,104],{"class":157},[147,2242,2055],{"class":161},[147,2244,2245],{"class":157},").",[147,2247,2248],{"class":262},"ToArray",[147,2250,2251],{"class":157},"();\n",[147,2253,2254,2256,2259,2261,2263,2265,2268],{"class":149,"line":206},[147,2255,2221],{"class":153},[147,2257,2258],{"class":290}," sum",[147,2260,294],{"class":175},[147,2262,2224],{"class":161},[147,2264,104],{"class":157},[147,2266,2267],{"class":262},"Sum",[147,2269,2251],{"class":157},[147,2271,2272,2274,2276,2278,2280,2282,2285,2288,2290,2292,2294,2296,2298],{"class":149,"line":308},[147,2273,191],{"class":153},[147,2275,2224],{"class":161},[147,2277,104],{"class":157},[147,2279,2233],{"class":262},[147,2281,266],{"class":157},[147,2283,2284],{"class":161},"v",[147,2286,2287],{"class":157}," => ",[147,2289,2284],{"class":290},[147,2291,2040],{"class":175},[147,2293,2258],{"class":290},[147,2295,2245],{"class":157},[147,2297,2248],{"class":262},[147,2299,2251],{"class":157},[147,2301,2302],{"class":149,"line":328},[147,2303,377],{"class":157},[15,2305,2306],{},"Same trees, different aggregation.",[10,2308,2310],{"id":2309},"validate-everything","Validate everything",[15,2312,2313],{},"Before optimizing, verify generated inference against the original model. Use multiple test cases, not just one row.",[138,2315,2317],{"className":140,"code":2316,"language":142,"meta":143,"style":143},"Assert.True(\n    diff \u003C 1e-4,\n    $\"Row {i} mismatch. Expected={expected}, Actual={actual}\"\n);\n",[50,2318,2319,2331,2348,2382],{"__ignoreMap":143},[147,2320,2321,2324,2326,2329],{"class":149,"line":150},[147,2322,2323],{"class":161},"Assert",[147,2325,104],{"class":157},[147,2327,2328],{"class":262},"True",[147,2330,1196],{"class":157},[147,2332,2333,2336,2338,2341,2343,2346],{"class":149,"line":188},[147,2334,2335],{"class":290},"    diff",[147,2337,1902],{"class":175},[147,2339,2340],{"class":168}," 1e",[147,2342,789],{"class":175},[147,2344,2345],{"class":168},"4",[147,2347,756],{"class":157},[147,2349,2350,2353,2356,2358,2360,2363,2365,2368,2370,2373,2375,2378,2380],{"class":149,"line":200},[147,2351,2352],{"class":982},"    $\"Row ",[147,2354,1102],{"class":2355},"sMj0N",[147,2357,1484],{"class":290},[147,2359,1108],{"class":2355},[147,2361,2362],{"class":982}," mismatch. Expected=",[147,2364,1102],{"class":2355},[147,2366,2367],{"class":290},"expected",[147,2369,1108],{"class":2355},[147,2371,2372],{"class":982},", Actual=",[147,2374,1102],{"class":2355},[147,2376,2377],{"class":290},"actual",[147,2379,1108],{"class":2355},[147,2381,1134],{"class":982},[147,2383,2384],{"class":149,"line":206},[147,2385,325],{"class":157},[10,2387,2389],{"id":2388},"whats-in-a-lightgbm-dump-and-what-makes-it-into-c","What's in a LightGBM dump, and what makes it into C#",[15,2391,2392],{},"Here is one real LightGBM tree from a text dump. Arrays are shortened for readability, but the shape is the same:",[138,2394,2397],{"className":2395,"code":2396,"language":235,"meta":143},[233],"Tree=1\nnum_leaves=5\nsplit_feature=11 2 17 18\nsplit_gain=437375 65423.8 30686.3 15052.8\nthreshold=1.0000000180025095e-35 0.94499999284744274 15.500000000000002 0.47699999809265142\ndecision_type=2 2 2 2\nleft_child=1 -1 -2 -3\nright_child=2 3 -4 -5\nleaf_value=0.0017963732109250652 -0.0029226866697364827 0.012002031587390959 -0.0066260868638778623 0.0074389293400878229\nleaf_weight=419486 548345 401639 1175784 300693\nleaf_count=419486 548345 401639 1175784 300693\ninternal_value=3.11874e-06 0.00803391 -0.00396084 0.00239001\ninternal_weight=5.49575e+06 1.8162e+06 3.67954e+06 617181\ninternal_count=5495746 1816203 3679543 617181\nis_linear=0\nshrinkage=0.02\n",[50,2398,2396],{"__ignoreMap":143},[15,2400,2401,2402,1682,2404,1682,2406,1682,2409,2412,2413,2416],{},"The dump contains more than the C# runtime needs. The generated code keeps the fields used to walk the tree and return a prediction: ",[50,2403,526],{},[50,2405,534],{},[50,2407,2408],{},"left_child",[50,2410,2411],{},"right_child",", and ",[50,2414,2415],{},"leaf_value",". A few other fields are useful to understand, but they do not show up in the final arrays.",[15,2418,2419,2422,2423,2425],{},[50,2420,2421],{},"shrinkage"," is the tree learning rate. In many LightGBM text dumps, the shrinkage has already been folded into ",[50,2424,2415],{},", so the C# predictor can just add the tree result directly:",[138,2427,2429],{"className":140,"code":2428,"language":142,"meta":143,"style":143},"score += Eval(Roots[i], f);\n",[50,2430,2431],{"__ignoreMap":143},[147,2432,2433,2436,2438,2440,2442,2444,2446,2448,2450,2452],{"class":149,"line":150},[147,2434,2435],{"class":290},"score",[147,2437,315],{"class":314},[147,2439,1671],{"class":262},[147,2441,266],{"class":157},[147,2443,1928],{"class":161},[147,2445,165],{"class":157},[147,2447,1484],{"class":290},[147,2449,1935],{"class":157},[147,2451,275],{"class":290},[147,2453,325],{"class":157},[15,2455,2456,2457,2459],{},"If shrinkage were not already folded into the leaves, the runtime would need to multiply each tree output by a per tree shrinkage value. For the dumped models I was working with, ignoring the explicit ",[50,2458,2421],{}," field was correct because the leaf values already contained it.",[15,2461,2462,2465,2466,2469],{},[50,2463,2464],{},"is_linear"," tells you whether the tree uses normal constant leaves or linear leaves. The exporter assumes ",[50,2467,2468],{},"is_linear=0",", where each leaf returns one scalar value. That matches the usual LightGBM tree:",[138,2471,2474],{"className":2472,"code":2473,"language":235,"meta":143},[233],"if feature \u003C= threshold:\n    return leaf_value\n",[50,2475,2473],{"__ignoreMap":143},[15,2477,112,2478,2481],{},[50,2479,2480],{},"is_linear=1",", each leaf contains a small linear model instead of a single number. That needs a different inference engine. This exporter does not support that case.",[15,2483,2484,2487],{},[50,2485,2486],{},"internal_value"," is the value stored at an internal split node. You can think of it as the prediction at that point if the tree stopped there. It is useful for diagnostics, but inference does not return from internal nodes, so the C# code does not need it.",[15,2489,2490,2493],{},[50,2491,2492],{},"internal_weight"," is the weighted amount of training data that reached an internal node. LightGBM uses it while training for split decisions, regularization, and pruning. Once the tree is trained, inference only needs to know which branch to take.",[15,2495,2496,2499,2500,104],{},[50,2497,2498],{},"leaf_weight"," is similar, but for a leaf. It tells you how much weighted training data ended up in that leaf. It can be useful when inspecting the model, but prediction only needs ",[50,2501,2415],{},[15,2503,2504],{},"So the flat C# representation is intentionally small:",[138,2506,2509],{"className":2507,"code":2508,"language":235,"meta":143},[233],"Feature\nThreshold\nLeft\nRight\nValue\nIsLeaf\nRoots\n",[50,2510,2508],{"__ignoreMap":143},[15,2512,2513],{},"That is basically a tiny runtime for executing tree bytecode. The original LightGBM dump has training metadata too, but the generated predictor only carries what it needs to reproduce inference.",[10,2515,2517],{"id":2516},"the-complete-code","The complete code",[15,2519,2520,2521,104],{},"The full exporter is on GitHub: ",[19,2522,2526],{"href":2523,"rel":2524},"https:\u002F\u002Fgithub.com\u002Fhaiilong\u002Fexport_lgbm_universal_cs",[2525],"nofollow","haiilong\u002Fexport_lgbm_universal_cs",[2528,2529,2530],"style",{},"html pre.shiki code .sLKXg, html code.shiki .sLKXg{--shiki-default:#A626A4;--shiki-dark:#C678DD}html pre.shiki code .s5ixo, html code.shiki .s5ixo{--shiki-default:#383A42;--shiki-dark:#ABB2BF}html pre.shiki code .s7GmK, html code.shiki .s7GmK{--shiki-default:#383A42;--shiki-dark:#E5C07B}html pre.shiki code .sAGMh, html code.shiki .sAGMh{--shiki-default:#986801;--shiki-dark:#D19A66}html pre.shiki code .sknuh, html code.shiki .sknuh{--shiki-default:#383A42;--shiki-dark:#56B6C2}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sAdtL, html code.shiki .sAdtL{--shiki-default:#4078F2;--shiki-dark:#61AFEF}html pre.shiki code .sz0mV, html code.shiki .sz0mV{--shiki-default:#383A42;--shiki-dark:#E06C75}html pre.shiki code .sblXP, html code.shiki .sblXP{--shiki-default:#383A42;--shiki-dark:#C678DD}html pre.shiki code .slOjB, html code.shiki .slOjB{--shiki-default:#383A42;--shiki-dark:#61AFEF}html pre.shiki code .sp7wS, html code.shiki .sp7wS{--shiki-default:#986801;--shiki-default-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic}html pre.shiki code .sDhpE, html code.shiki .sDhpE{--shiki-default:#50A14F;--shiki-dark:#98C379}html pre.shiki code .so_Uh, html code.shiki .so_Uh{--shiki-default:#986801;--shiki-default-font-style:inherit;--shiki-dark:#D19A66;--shiki-dark-font-style:italic}html pre.shiki code .s_Sar, html code.shiki .s_Sar{--shiki-default:#0184BC;--shiki-dark:#56B6C2}html pre.shiki code .sC09Y, html code.shiki .sC09Y{--shiki-default:#C18401;--shiki-dark:#E5C07B}html pre.shiki code .sMj0N, html code.shiki .sMj0N{--shiki-default:#50A14F;--shiki-dark:#ABB2BF}",{"title":143,"searchDepth":188,"depth":188,"links":2532},[2533,2534,2535,2536,2537,2538,2539,2540,2546,2547,2548,2549,2553,2554,2555],{"id":12,"depth":188,"text":13},{"id":86,"depth":188,"text":87},{"id":225,"depth":188,"text":226},{"id":390,"depth":188,"text":391},{"id":575,"depth":188,"text":576},{"id":718,"depth":188,"text":719},{"id":927,"depth":188,"text":928},{"id":1536,"depth":188,"text":1537,"children":2541},[2542,2543,2544,2545],{"id":1541,"depth":200,"text":1542},{"id":1548,"depth":200,"text":1549},{"id":1566,"depth":200,"text":1567},{"id":1573,"depth":200,"text":1574},{"id":1580,"depth":188,"text":1581},{"id":1633,"depth":188,"text":1634},{"id":1961,"depth":188,"text":1962},{"id":1988,"depth":188,"text":1989,"children":2550},[2551,2552],{"id":1992,"depth":200,"text":1993},{"id":2075,"depth":200,"text":2076},{"id":2309,"depth":188,"text":2310},{"id":2388,"depth":188,"text":2389},{"id":2516,"depth":188,"text":2517},null,"2026-04-28","Converting LightGBM models into native .NET inference","md",{},"\u002Fblog\u002Fbuilding-ml-inference-part-4",{"title":5,"description":2558},"blog\u002Fbuilding-ml-inference-part-4",[2565],"tech","R6mQdvNAEOnn4JOLS_Ong_EaXeyTDq8h1aWEXtRMX0I",1778998257278]