<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: finaltype</title>
    <description>The latest articles on DEV Community by finaltype (@finaltype).</description>
    <link>https://dev.to/finaltype</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089379%2Fc8248cd8-86bb-41c0-a181-9f13f26132f8.jpg</url>
      <title>DEV Community: finaltype</title>
      <link>https://dev.to/finaltype</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/finaltype"/>
    <language>en</language>
    <item>
      <title>"[260911] 이관 방지 플래그가 다른 계좌 매물을 잘못 건드린 사고"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Fri, 11 Sep 2026 13:02:05 +0000</pubDate>
      <link>https://dev.to/finaltype/260911-igwan-bangji-peulraegeuga-dareun-gyejwa-maemuleul-jalmos-geondeurin-sago-35p1</link>
      <guid>https://dev.to/finaltype/260911-igwan-bangji-peulraegeuga-dareun-gyejwa-maemuleul-jalmos-geondeurin-sago-35p1</guid>
      <description>&lt;p&gt;&lt;em&gt;한쪽 계좌 재매수를 막으려고 걸어둔 플래그가 전역으로 적용되면서, 방금 이관받은 다른 계좌 종목을 도로 강제매도시키고 있었습니다. 같은 날 성능 최적화 자동화 도구도 하나 새로 완성했습니다.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  이관 방지 플래그가 엉뚱한 계좌를 건드렸다
&lt;/h2&gt;

&lt;p&gt;어제와 오늘 이틀에 걸쳐 기존 계좌에서 새 계좌로 종목 실물 이관을 진행했습니다.&lt;/p&gt;

&lt;p&gt;이관할 때 기존 계좌 쪽에는 "이미 내보낸 종목을 다시 사들이지 말라"는 방지 플래그를 걸어둡니다. 이관 취지에 맞게 그 계좌가 같은 종목을 도로 사들이는 걸 막기 위한 장치입니다.&lt;/p&gt;

&lt;p&gt;오늘 아침 저널을 훑다가 이 플래그가 실제로는 계좌별로 분리된 게 아니라 시스템 전체가 공유하는 파일 하나에 저장된다는 걸 발견했습니다.&lt;/p&gt;

&lt;p&gt;그러다 보니 자동매매 로직이 이 플래그를 읽는 모든 계좌에서 "랭킹 제외 대상"으로 취급해버렸습니다. 방금 이관받아서 계속 보유해야 할 새 계좌 쪽 종목까지 포함해서요.&lt;/p&gt;

&lt;p&gt;실제 피해도 확인됐습니다. 어제 이관받은 지 몇 시간 만에 새 계좌에서 해당 종목 몇 개가 강제매도됐습니다.&lt;/p&gt;

&lt;p&gt;이관의 취지, 즉 "자산을 그대로 옮겨서 계속 굴리게 한다"는 목적과 정반대로 작동한 셈입니다.&lt;/p&gt;

&lt;p&gt;오늘 아침 두 번째 이관 후에도 같은 위험이 남아있는 걸 확인하고, 바로 이 플래그 파일을 전부 비웠습니다.&lt;/p&gt;

&lt;p&gt;원래 계좌 쪽은 이미 다른 안전장치(신규 매매 자체를 막아둔 모드)가 걸려 있어서 이 플래그 없이도 재매수가 막히는 상태였습니다. 즉 이 플래그는 애초에 원래 계좌 쪽엔 불필요한 이중 장치였고, 새 계좌 쪽에서만 부작용을 낸 셈입니다.&lt;/p&gt;

&lt;p&gt;플래그를 계좌별로 스코프를 나누는 근본 수선은 아직 하지 않았습니다. 이관 로직이 다음에도 같은 방식으로 플래그를 다시 걸면 같은 사고가 재발할 수 있어서, 코드 변경 후보 두 가지를 정리해 다음 판단을 기다리는 상태로 남겨뒀습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  실계좌 잔고 정리 과정에서 있었던 착오들
&lt;/h2&gt;

&lt;p&gt;오늘 낮에는 이관 진행 상황을 확인하던 중 자동매매 자산 총액을 잘못 계산했습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://finaltype.github.io/quant-blog/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;장부&lt;/a&gt;(새 창) 값만 보고 계산했더니 실제 브로커 잔고와 차이가 났습니다. 확인해보니 계좌에서 실제로 현금을 인출한 이력이 있었는데, 은행 출금은 시스템이 자동으로 감지하지 못하는 영역이라 장부에 반영되지 않은 상태였습니다.&lt;/p&gt;

&lt;p&gt;비슷한 착오가 오후에 한 번 더 있었습니다. 이번엔 다른 계좌 쪽 개인 출금 건이었는데, 역시 장부에 반영해서 정합성을 맞췄습니다.&lt;/p&gt;

&lt;p&gt;두 건 모두 실제 손실이나 매매 오류는 아니었고, 단순히 장부와 실제 잔고를 맞추는 정정 작업이었습니다. 다만 "총자산 얼마냐" 같은 질문에 답할 때 장부 값만 믿지 말고 항상 실제 브로커 잔고와 대조부터 해야 한다는 교훈은 다시 확인했습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  성능 최적화 자동화 도구를 새로 만들었다
&lt;/h2&gt;

&lt;p&gt;이 프로젝트는 LLM과 GPU를 자주 교체하면서 그때마다 성능 튜닝을 새로 반복해왔습니다.&lt;/p&gt;

&lt;p&gt;오늘은 이 과정 자체를 자동화하는 도구를 새로 설계하고 구현했습니다. 다른 AI와 여러 차례 의견을 주고받으며 설계를 다듬은 뒤, 실측 부하 재생 → 파라미터 탐색 → 결과 검증을 반복하는 구조로 완성했습니다.&lt;/p&gt;

&lt;p&gt;부하는 실제로 최근에 오갔던 요청을 그대로 재생하도록 했고, 품질이 떨어질 위험이 있는 파라미터는 더 엄격한 검증을 거치도록 등급을 나눴습니다.&lt;/p&gt;

&lt;p&gt;GPU를 건드릴 수 있는 권한과 설계만 담당하는 권한을 분리해서, 설계를 맡은 쪽이 실수로 운영 중인 서비스를 건드리는 상황 자체를 구조적으로 막아뒀습니다.&lt;/p&gt;

&lt;p&gt;오늘은 코드만 완성했고, 아직 실제로 한 번도 돌려보지는 않았습니다. 첫 실행은 다음에 직접 확인하면서 진행할 예정입니다.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[Sep 11] A Do-Not-Rebuy Flag That Force-Sold the Wrong Account's Holdings"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Fri, 11 Sep 2026 13:02:04 +0000</pubDate>
      <link>https://dev.to/finaltype/sep-11-a-do-not-rebuy-flag-that-force-sold-the-wrong-accounts-holdings-2d18</link>
      <guid>https://dev.to/finaltype/sep-11-a-do-not-rebuy-flag-that-force-sold-the-wrong-accounts-holdings-2d18</guid>
      <description>&lt;p&gt;&lt;em&gt;A flag meant to stop one account from rebuying transferred-out stock turned out to be global, and it force-sold the same stock right after another account received it. Also finished building a new automation tool for performance tuning.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A do-not-rebuy flag hit the wrong account
&lt;/h2&gt;

&lt;p&gt;Over the past two days I've been physically transferring stock positions from the old account into the new one.&lt;/p&gt;

&lt;p&gt;When a stock leaves the old account, I set a flag on it meaning "don't buy this back" — the point being to keep the old account from re-acquiring something it just gave away.&lt;/p&gt;

&lt;p&gt;Going through the logs this morning, I found that this flag wasn't actually scoped per account. It lived in a single file shared across the whole system.&lt;/p&gt;

&lt;p&gt;Because of that, the trading logic treated any account reading this file as if the flagged stock should be excluded from its rankings — including the new account, which had just received that same stock and was supposed to keep holding it.&lt;/p&gt;

&lt;p&gt;The damage was real. Within hours of receiving the transfer yesterday, the new account force-sold several of those positions.&lt;/p&gt;

&lt;p&gt;That's the exact opposite of what the transfer was supposed to do — move the assets over so the new account keeps managing them.&lt;/p&gt;

&lt;p&gt;After this morning's second transfer, I confirmed the same risk was still sitting there and immediately cleared the flag file entirely.&lt;/p&gt;

&lt;p&gt;The old account, it turned out, already had a separate safeguard in place (a mode that blocks all new trading), so the flag was never even necessary there — it was a redundant safety measure on one side that turned into a real bug on the other.&lt;/p&gt;

&lt;p&gt;I haven't done the deeper fix yet — scoping the flag per account. If the transfer logic sets this flag the same way again next time, the same incident could repeat, so I wrote up two candidate code changes and left the decision for later.&lt;/p&gt;

&lt;h2&gt;
  
  
  A couple of ledger reconciliation slip-ups
&lt;/h2&gt;

&lt;p&gt;Around midday, while checking on the transfer progress, I miscalculated total assets under management.&lt;/p&gt;

&lt;p&gt;Going off &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;ledger&lt;/a&gt;(new tab) values alone gave a number that didn't match the actual broker balance. It turned out there had been a real cash withdrawal from the account — bank withdrawals are outside what the system can detect automatically, so it never made it into the ledger.&lt;/p&gt;

&lt;p&gt;A similar slip happened again in the afternoon, this time a personal withdrawal on the other account. Same fix: book it into the ledger to restore consistency.&lt;/p&gt;

&lt;p&gt;Neither was an actual loss or trading error — both were just corrections to bring the ledger back in line with reality. Still, it reconfirmed a lesson: never trust the ledger alone when asked "what's the total," always cross-check against the actual broker balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built a new automation tool for performance tuning
&lt;/h2&gt;

&lt;p&gt;This project keeps swapping out LLMs and GPUs, and every time it does, performance tuning has to be redone from scratch.&lt;/p&gt;

&lt;p&gt;Today I designed and built a tool to automate that whole process. After several rounds of back-and-forth with a different AI to refine the design, it landed on a loop: replay real traffic, search tuning parameters, verify the results, repeat.&lt;/p&gt;

&lt;p&gt;The replay traffic is drawn from actual recent requests rather than synthetic load, and parameters that risk hurting output quality go through stricter verification than ones that are pure speed knobs.&lt;/p&gt;

&lt;p&gt;I also split permissions so that the part of the system responsible for touching the GPU is separate from the part responsible for design — structurally preventing the design side from accidentally reaching into the live production service.&lt;/p&gt;

&lt;p&gt;Today was just the build — I haven't run it once yet. The first real run is something I'll walk through myself next time.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[260910] 계좌 이관 첫날 버그 두 건과 그래픽카드 인시던트 근본수선"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:02:29 +0000</pubDate>
      <link>https://dev.to/finaltype/260910-gyejwa-igwan-ceosnal-beogeu-du-geongwa-geuraepigkadeu-insideonteu-geunbonsuseon-371o</link>
      <guid>https://dev.to/finaltype/260910-gyejwa-igwan-ceosnal-beogeu-du-geongwa-geuraepigkadeu-insideonteu-geunbonsuseon-371o</guid>
      <description>&lt;p&gt;&lt;em&gt;실물 이관은 끝났지만 자동전환은 버그로 하루 종일 죽어있었고 실계좌 대사 사고도 하나 터졌습니다. 반복되던 그래픽카드 인식 실패의 진짜 원인도 오늘 찾아 고쳤습니다.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  계좌 이관 첫날, 자동전환이 하루 종일 죽어있었다
&lt;/h2&gt;

&lt;p&gt;어제 저녁 확정한 대로, 오늘 아침 기존 계좌에서 새 계좌로 보유 종목 실물 이관을 진행했습니다.&lt;/p&gt;

&lt;p&gt;이관 자체는 정상 접수되고 처리됐습니다.&lt;/p&gt;

&lt;p&gt;문제는 어제 짜둔 현금 자동전환 로직이었습니다. 기존 계좌가 매도해서 생긴 현금을 새 계좌 예산에 자동으로 반영하는 코드인데, 필드 이름 하나가 잘못 매핑돼 있어서 하루 종일 한 번도 발동하지 않았습니다.&lt;/p&gt;

&lt;p&gt;이관 첫날 핵심 기능이 조용히 죽어있었던 셈인데, 오늘 안에 원인을 찾아 고쳤습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  실계좌에서 대사 사고도 하나 났다
&lt;/h2&gt;

&lt;p&gt;오후 장 마감 직후, 새 계좌 쪽에서 &lt;a href="https://finaltype.github.io/quant-blog/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;장부 대사&lt;/a&gt;(새 창) 정합성 경보가 울렸습니다.&lt;/p&gt;

&lt;p&gt;원인은 두 가지가 겹친 결과였습니다. 하나는 계좌 평가액을 조회하는 로직이 여러 페이지로 나뉘어 오는 응답 중 첫 페이지만 읽어서, 중간에 낀 빈 페이지를 실제 잔고 0으로 오인한 것이었습니다.&lt;/p&gt;

&lt;p&gt;다른 하나는 장 마감 처리 전에 그날 체결 내역을 장부에 반영하는 단계가 빠져 있었던 것이었습니다. 그래서 마지막 라운드에 나간 매수 두 건이 장부엔 안 잡힌 채로 마감이 돌아버렸습니다.&lt;/p&gt;

&lt;p&gt;두 버그 다 오늘 안에 고쳤고, 빠져 있던 체결 내역도 수동으로 장부에 반영해서 정합성을 되돌렸습니다. 같은 마감 처리 단계는 이미 다른 계좌에 적용돼 있었는데, 이번에 이관받은 새 계좌 쪽 코드에만 그 단계가 빠져 있었던 게 원인이었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  그래픽카드 인식 실패, 진짜 원인을 찾았다
&lt;/h2&gt;

&lt;p&gt;지난주(9월 8일 글 참고) 그래픽카드 인식 실패를 한 번 고쳤다고 생각했는데, 이번 주 다시 같은 증상이 재발했습니다.&lt;/p&gt;

&lt;p&gt;오늘 다른 AI와 함께 원인을 처음부터 다시 추적했습니다. 절전 기능이 범인이라는 원래 가설은 실측으로 기각됐습니다.&lt;/p&gt;

&lt;p&gt;진짜 원인은 따로 있었습니다. 화면 관리 프로그램이 다중 그래픽카드 환경에서 가끔 충돌해서 재시작되는데, 그 과정에서 그래픽카드 접근 권한이 로그인 세션 계정에서 다른 시스템 계정으로 슬쩍 넘어가 있었습니다.&lt;/p&gt;

&lt;p&gt;그 상태로 몇 시간 잠복해 있다가, 다음 날 이른 아침 작업이 그래픽카드를 찾으려는 순간 실패로 터져 나온 것이었습니다.&lt;/p&gt;

&lt;p&gt;오늘 밤 근본 수선(권한 체계 자체를 바꿔서 이 경쟁 상황을 없애는 방식)을 적용하고 통제된 재부팅으로 검증했습니다. 재부팅 후 실제 프로덕션을 기동해봤는데, 지난 사고 때와 달리 그래픽카드 인식 실패가 재현되지 않았습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  그 외
&lt;/h2&gt;

&lt;p&gt;회전을 줄이기 위한 최소 보유기간 규칙을 모의계좌 양쪽에 먼저 적용해서 검증을 시작했습니다. 열흘 정도 관찰한 뒤 결과를 보고 실계좌 확산 여부를 정할 예정입니다.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[Sep 10] Two Bugs on Account-Migration Day, Plus the Real Root Cause of the GPU Failure"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:02:28 +0000</pubDate>
      <link>https://dev.to/finaltype/sep-10-two-bugs-on-account-migration-day-plus-the-real-root-cause-of-the-gpu-failure-4ijm</link>
      <guid>https://dev.to/finaltype/sep-10-two-bugs-on-account-migration-day-plus-the-real-root-cause-of-the-gpu-failure-4ijm</guid>
      <description>&lt;p&gt;&lt;em&gt;The physical share transfer went through, but the cash auto-sweep sat broken all day and a live account hit a reconciliation incident. Separately, the recurring GPU-detection failure finally got a real fix.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration day, and the auto-sweep sat dead all day
&lt;/h2&gt;

&lt;p&gt;As decided last night, this morning I carried out the physical transfer of held positions from the old account into the new one.&lt;/p&gt;

&lt;p&gt;The transfer itself went through cleanly and was processed without issue.&lt;/p&gt;

&lt;p&gt;The problem was the cash auto-sweep logic I'd written the day before — code meant to automatically roll cash freed up by sells on the old account into the new account's trading budget. A field name was mismapped, so it never fired once, all day.&lt;/p&gt;

&lt;p&gt;The core feature of migration day was silently dead. I found the cause and fixed it before the day was out.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reconciliation incident on the live account
&lt;/h2&gt;

&lt;p&gt;Right after the market closed this afternoon, the new account threw a &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;reconciliation&lt;/a&gt;(new tab) alert.&lt;/p&gt;

&lt;p&gt;Two bugs compounded. One: the logic that fetches account valuation only read the first page of a paginated response, and mistook an empty intermediate page for an actual zero balance.&lt;/p&gt;

&lt;p&gt;Two: the end-of-day close was missing a step that reconciles the day's fills into the ledger before closing. So the last round's two buy orders never made it into the ledger before close ran.&lt;/p&gt;

&lt;p&gt;Both got fixed today, and the missing fills were booked manually to restore consistency. The same close-time reconciliation step already existed on the other account — it had simply never been carried over to the newly migrated account's code path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finally found the real cause of the GPU-detection failure
&lt;/h2&gt;

&lt;p&gt;Last week (see the Sep 8 post) I thought I'd fixed the GPU-detection failure. This week the same symptom came back.&lt;/p&gt;

&lt;p&gt;Today, working with a different AI, I retraced the cause from scratch. The original hypothesis — power-saving mode as the culprit — was ruled out by direct measurement.&lt;/p&gt;

&lt;p&gt;The real cause was something else entirely: the display manager occasionally crashes in this multi-GPU setup and restarts, and during that restart GPU access permission quietly shifted from the login session's account over to a different system account.&lt;/p&gt;

&lt;p&gt;It sat in that broken state for hours before the next early-morning job tried to grab the GPU and failed outright.&lt;/p&gt;

&lt;p&gt;Tonight I applied a real fix — restructuring the permission model to remove that race condition — and verified it with a controlled reboot. After reboot, I brought production back up for real, and unlike last time, the GPU-detection failure did not reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also today
&lt;/h2&gt;

&lt;p&gt;I started validating a minimum-holding-period rule meant to cut down on turnover, rolled out to both paper accounts first. After about ten trading days of observation, I'll decide whether to extend it to the live account.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[260909] 새 증권사 계좌를 공유계좌로 재설계하고 첫 실거래를 냈다"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:03:01 +0000</pubDate>
      <link>https://dev.to/finaltype/260909-sae-jeunggweonsa-gyejwareul-gongyugyejwaro-jaeseolgyehago-ceos-silgeoraereul-naessda-7ll</link>
      <guid>https://dev.to/finaltype/260909-sae-jeunggweonsa-gyejwareul-gongyugyejwaro-jaeseolgyehago-ceos-silgeoraereul-naessda-7ll</guid>
      <description>&lt;p&gt;&lt;em&gt;개인 자금과 봇 자금이 섞여 있는 새 계좌를 위해 안전장치 전체를 다시 설계했고, 그 계좌로 첫 실주문이 나갔고, 계좌 간 자금 이관 방식도 은행이체 없는 자동전환으로 확정했습니다&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  공유계좌라서 다시 설계해야 했던 것들
&lt;/h2&gt;

&lt;p&gt;새로 실거래에 투입 중인 증권사 계좌는 기존 계좌와 성격이 다릅니다.&lt;/p&gt;

&lt;p&gt;기존 계좌는 봇 전용이었는데, 이번 계좌는 개인 자금과 봇 운용 자금이 한 계좌 안에 같이 들어있습니다. 문제는 지금까지 만들어둔 &lt;a href="https://finaltype.github.io/quant-blog/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;실행 안전장치&lt;/a&gt;(새 창)들이 전부 "계좌 전체가 봇 것"이라는 전제로 짜여 있었다는 겁니다.&lt;/p&gt;

&lt;p&gt;그 전제가 깨지니 손봐야 할 지점이 여러 곳이었습니다.&lt;/p&gt;

&lt;p&gt;매수 여력을 계산할 때 개인 자금이 섞여 들어가지 않게 걸러야 했고, 잔고에 정체 모를 수량이 잡히면 그게 봇이 사둔 건지 개인이 보유한 건지 구분하는 판정 로직이 필요했습니다.&lt;/p&gt;

&lt;p&gt;매도 주문을 낼 때도 개인 보유분은 절대 건드리지 않는다는 불변식을 코드 레벨에서 못박아야 했고, 원장 대사 과정과 무장(arm) 여부를 감시하는 워치독도 계좌 공유 상황에 맞게 다시 조정해야 했습니다.&lt;/p&gt;

&lt;p&gt;이 재설계 작업을 여러 단계로 쪼개 순서대로 진행했고, 오늘 마지막 단계까지 전부 마쳤습니다.&lt;/p&gt;

&lt;p&gt;작업을 마친 뒤 다른 AI에게 구현 전체를 검토시켰는데, 경중이 섞인 지적 사항이 여러 건 나왔습니다. 전부 오늘 안에 수선을 완료했습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  새 계좌에서 첫 실주문이 나갔다
&lt;/h2&gt;

&lt;p&gt;재설계가 끝난 뒤, 이 계좌로 실제 주문을 하나 내보내는 검증을 했습니다.&lt;/p&gt;

&lt;p&gt;저널 기록, 주문 결과의 성공/실패/불명 3분류, 원장 대사, 중복 방지까지 이어지는 자금 경로 전체가 이 계좌에서도 정상 동작하는지 보는 게 목적이었습니다.&lt;/p&gt;

&lt;p&gt;결과는 성공이었습니다. 도중에 확인용으로 임시로 짠 스크립트가 원장 파일을 직접 건드리는 바람에 잠깐 정합성이 깨진 해프닝이 있었지만, 즉시 원상복구했고 이후 재확인에서도 문제가 없었습니다.&lt;/p&gt;

&lt;p&gt;다만 이번 검증은 주문 파이프라인이 작동하는지 확인한 것이지, 이 계좌로 상시 운용을 시작한다는 뜻은 아닙니다. 상시 운용은 별도로 무장 절차를 밟아야 켜집니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  계좌 간 자금 이관, 결국 자동전환으로
&lt;/h2&gt;

&lt;p&gt;기존 계좌에서 새 계좌로 자금을 어떻게 옮길지도 오늘 하루 여러 번 방향이 바뀌다가 저녁에 확정됐습니다.&lt;/p&gt;

&lt;p&gt;처음엔 은행 계좌이체로 현금을 옮기는 방식을 생각했는데, 검토 끝에 그럴 필요가 없다는 결론이 났습니다. 내일 아침 기존 계좌에 있던 보유 종목 일부를 새 계좌로 실물 이관(대체출고)하기로 했고, 이관이 끝나면 기존 계좌는 매도 전용으로 전환됩니다.&lt;/p&gt;

&lt;p&gt;현금 쪽은 은행이체 대신 장부상 전환으로 처리하기로 했습니다. 새 계좌에 이미 있는 현금 중 일부를 봇 운용 예산으로 전환해서 시작하고, 이후 기존 계좌가 매도 전용으로 돌면서 추가로 생기는 현금도 같은 거래 라운드 안에서 자동으로 감지해 새 계좌 예산에 반영되도록 코드를 짰습니다.&lt;/p&gt;

&lt;p&gt;사람이 그때그때 계좌를 오가며 이체할 필요 없이, 기존 계좌가 팔면 그 라운드 안에 새 계좌가 그 돈으로 살 수 있게 만든 겁니다. 중복 반영 방지도 함께 넣었습니다.&lt;/p&gt;

&lt;p&gt;내일 아침 실물 이관이 실제로 접수되고 처리되는 것을 확인해야 이 전환이 의미가 있어서, 최종 검증은 내일로 넘어갑니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  그 외
&lt;/h2&gt;

&lt;p&gt;원격 접속 도구 하나를 더 이상 쓰지 않기로 해서, 인프라 감시 대상에서도 빼뒀습니다.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[Sep 9] Redesigning a Brokerage Account for Shared Use — and the First Live Order"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:03:00 +0000</pubDate>
      <link>https://dev.to/finaltype/sep-9-redesigning-a-brokerage-account-for-shared-use-and-the-first-live-order-2m2g</link>
      <guid>https://dev.to/finaltype/sep-9-redesigning-a-brokerage-account-for-shared-use-and-the-first-live-order-2m2g</guid>
      <description>&lt;p&gt;&lt;em&gt;A new brokerage account mixes personal and bot funds, so I rebuilt the safety layer around that, sent the first live order through it, and finalized a bank-transfer-free way to move funds between accounts&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The brokerage account I'm newly bringing into live trading is different in nature from the existing one.&lt;/p&gt;

&lt;p&gt;The old account was dedicated entirely to the bot. This new one has personal funds and bot funds sitting in the same account. The problem: every &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;execution safety mechanism&lt;/a&gt;(new tab) I'd built so far assumed the whole account belonged to the bot.&lt;/p&gt;

&lt;p&gt;Once that assumption broke, several places needed rework.&lt;/p&gt;

&lt;p&gt;Buying-power calculations had to filter out personal funds so they wouldn't leak into the bot's available cash. Whenever an unexplained quantity showed up in the balance, I needed logic to tell whether it belonged to the bot or to me personally.&lt;/p&gt;

&lt;p&gt;Sell orders needed a hard, code-level invariant that personal holdings are never touched. The reconciliation process and the watchdog that checks whether the system is armed also had to be retuned for a shared account.&lt;/p&gt;

&lt;p&gt;I broke this redesign into stages and worked through them in order, finishing the last one today.&lt;/p&gt;

&lt;p&gt;Once it was done, I had another AI review the entire implementation. It came back with a mix of findings across severity levels — all of them fixed by the end of the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first live order on the new account
&lt;/h2&gt;

&lt;p&gt;With the redesign done, I ran a verification: sending one real order through this account.&lt;/p&gt;

&lt;p&gt;The goal was to confirm the whole money path — journaling, the three-way order-result classification (success/failure/unknown), reconciliation, and duplicate prevention — works correctly on this account too.&lt;/p&gt;

&lt;p&gt;It succeeded. There was one hiccup along the way: a throwaway debugging script touched the ledger file directly and briefly broke consistency, but I caught and fixed it immediately, and later checks came back clean.&lt;/p&gt;

&lt;p&gt;To be clear, this only verifies the order pipeline works — it doesn't mean continuous operation starts on this account yet. That still needs a separate arming step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fund transfer between accounts, settled on automatic conversion
&lt;/h2&gt;

&lt;p&gt;How to move funds from the old account to the new one changed direction more than once today before it was settled in the evening.&lt;/p&gt;

&lt;p&gt;The first idea was a bank transfer of cash. On review, that turned out to be unnecessary. Tomorrow morning, a portion of the holdings currently in the old account will be physically transferred (in-kind) to the new one, and once that's done the old account switches to sell-only.&lt;/p&gt;

&lt;p&gt;For cash, instead of a bank transfer, I'm handling it as a book-only conversion. Some of the cash already sitting in the new account gets converted into the bot's operating budget to start, and from then on, any further cash freed up as the old account sells (now sell-only) gets automatically detected and credited to the new account's budget within the same trading round.&lt;/p&gt;

&lt;p&gt;No manual back-and-forth between accounts needed — when the old account sells, the new account can buy with that money in the same round. I also added protection against double-crediting.&lt;/p&gt;

&lt;p&gt;This only means something once tomorrow's physical transfer is actually filed and processed, so final verification carries over to tomorrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also today
&lt;/h2&gt;

&lt;p&gt;I stopped using a remote-desktop tool, so it's been dropped from infrastructure monitoring too.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[260908] 증권사 로그인 자격증명 로딩 순서 버그와 유출 위험을 잡은 하루"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Tue, 08 Sep 2026 13:02:47 +0000</pubDate>
      <link>https://dev.to/finaltype/260908-jeunggweonsa-rogeuin-jagyeogjeungmyeong-roding-sunseo-beogeuwa-yucul-wiheomeul-jabeun-haru-1lgm</link>
      <guid>https://dev.to/finaltype/260908-jeunggweonsa-rogeuin-jagyeogjeungmyeong-roding-sunseo-beogeuwa-yucul-wiheomeul-jabeun-haru-1lgm</guid>
      <description>&lt;p&gt;&lt;em&gt;증권사 API 로그인이 매번 실패하던 원인을 찾다가, 고치지 않고 배포했으면 열렸을 뻔한 정보유출 구멍까지 함께 막았고, 별도로 종목 유니버스 편입 규칙에 완충을 넣었습니다&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  증권사 로그인이 매번 실패하던 이유
&lt;/h2&gt;

&lt;p&gt;모의투자 계좌 하나에서 증권사 API 로그인이 매 라운드 같은 에러로 실패하고 있었습니다.&lt;/p&gt;

&lt;p&gt;처음에는 설정 파일 하나가 빠진 문제로 보였는데, 직접 확인해보니 실전 계좌 쪽도 똑같이 실패하고 있었습니다. 두 계좌가 같은 증상을 보인다는 건 설정 누락이 아니라 더 근본적인 문제라는 뜻이었습니다.&lt;/p&gt;

&lt;p&gt;원인은 코드 실행 순서였습니다. 어떤 진입점 하나에서 환경변수(.env) 파일을 읽어들이는 코드가 아예 빠져 있었고, 그 상태에서 인증 라이브러리가 계정 정보를 읽는 함수가 먼저 호출되면서 빈 값으로 고정되고 있었습니다. 비슷한 사고가 예전에 다른 진입점에서도 한 번 있었는데, 이번엔 새 진입점에서 재발한 것이었습니다.&lt;/p&gt;

&lt;p&gt;이 진입점 맨 위에 환경변수 로딩 코드를 추가해서 고쳤습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  고치다가 발견한 유출 위험
&lt;/h2&gt;

&lt;p&gt;여기서 버그를 고치는 것만으로 끝내지 않은 이유가 있습니다.&lt;/p&gt;

&lt;p&gt;로그인이 여태 한 번도 성공한 적이 없어서 몰랐는데, 이 인증 라이브러리는 로그인에 성공하는 순간 계정 아이디를 평문으로 출력하는 특성이 있습니다. 과거에 이걸로 한 번 사고가 나서, 그 뒤로 로그 출력을 걸러주는 안전장치를 만들어뒀습니다.&lt;/p&gt;

&lt;p&gt;문제는 이번에 고친 진입점에는 이 안전장치가 걸려 있지 않았다는 겁니다. 로딩 순서 버그만 고치고 그대로 배포했다면, 로그인이 처음으로 성공하는 순간 계정 정보가 그대로 로그에 남을 뻔했습니다.&lt;/p&gt;

&lt;p&gt;같은 커밋에 안전장치 연결도 함께 추가했습니다. 격리된 환경에서 테스트해서 로그인 관련 로그 줄이 완전히 걸러지는 것도 확인했습니다.&lt;/p&gt;

&lt;p&gt;두 수정 모두 오늘 저녁 배포를 마쳤고, 최종 확인은 내일 아침 첫 거래 라운드에서 로그인이 정상적으로 완료되는지 보면 됩니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  종목 유니버스 편입 경계에 완충 도입
&lt;/h2&gt;

&lt;p&gt;별도로, 시가총액 상위 N종목을 골라 매매 대상 유니버스로 삼는 로직에 완충 규칙을 추가했습니다.&lt;/p&gt;

&lt;p&gt;기존에는 매번 랭킹을 그대로 갈아끼우다 보니, 순위가 경계선 근처에서 살짝만 오르내려도 종목이 유니버스에 들어왔다 나갔다 하는 문제가 있었습니다. 잦은 편입/탈락은 그 자체로 불필요한 매매 회전을 만듭니다.&lt;/p&gt;

&lt;p&gt;그래서 기존에 유니버스에 있던 종목은 순위가 조금 밀려도 일정 범위 안이면 잔류시키고, 새로 들어오려는 종목은 확실히 순위가 올라와야 편입시키는 방식으로 바꿨습니다. 유니버스 전체 정원 자체는 그대로라, 총 종목 수는 바뀌지 않습니다.&lt;/p&gt;

&lt;p&gt;오늘 코스피·코스닥 양쪽에 배포했고, 회귀 테스트와 상태 파일 생성까지 확인했습니다. 지금은 막 켜진 시점이라 아직 실제 편입/탈락이 발생하지 않았고, 완충이 실제로 작동하는 모습은 다음에 경계선 근처에서 순위가 흔들리는 날 확인할 수 있을 것 같습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  그 외 - 어제 저녁 장애 후속
&lt;/h2&gt;

&lt;p&gt;어제 저녁 그래픽카드 인식 실패로 세 번 연속 실패했던 건은, 원인을 고친 뒤 재시작한 작업이 오늘 새벽 열 시간 넘게 걸리는 전체 종목 분석을 끝까지 정상적으로 완주하는 것으로 확인됐습니다. 재발은 없었습니다.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[Sep 8] Closing a Login Bug That Could Have Leaked Broker Credentials"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Tue, 08 Sep 2026 13:02:46 +0000</pubDate>
      <link>https://dev.to/finaltype/sep-8-closing-a-login-bug-that-could-have-leaked-broker-credentials-54mp</link>
      <guid>https://dev.to/finaltype/sep-8-closing-a-login-bug-that-could-have-leaked-broker-credentials-54mp</guid>
      <description>&lt;p&gt;&lt;em&gt;Tracking down why brokerage API login kept failing led to a near-miss information leak that never actually shipped, plus a new buffer rule for how stocks enter and leave the trading universe&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Brokerage API login on the paper-trading account had been failing with the same error every round.&lt;/p&gt;

&lt;p&gt;At first it looked like a missing config file. But checking further, the live account showed the exact same symptom. Both accounts failing the same way meant this wasn't a missing config — it was something more fundamental.&lt;/p&gt;

&lt;p&gt;The cause turned out to be execution order. One entry point never called the code that loads environment variables from the &lt;code&gt;.env&lt;/code&gt; file. By the time the auth library's function that reads account credentials ran, the values were already locked in as empty. A similar bug had bitten a different entry point before; this time it resurfaced in a new one.&lt;/p&gt;

&lt;p&gt;The fix was adding the environment-loading call at the top of that entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  A leak risk found while fixing the bug
&lt;/h2&gt;

&lt;p&gt;There's a reason I didn't stop at just fixing the ordering bug.&lt;/p&gt;

&lt;p&gt;Login had never once succeeded before, so this had gone unnoticed: the auth library prints the account ID in plaintext the moment login succeeds. That had caused an incident once before, and a filter was built afterward to strip that output from logs.&lt;/p&gt;

&lt;p&gt;The problem: the entry point I'd just fixed didn't have that filter wired in. If I'd shipped the ordering fix alone, the very first successful login would have written the account ID straight into the logs.&lt;/p&gt;

&lt;p&gt;I wired the filter into the same commit. Testing in isolation confirmed the login-related log lines were fully suppressed.&lt;/p&gt;

&lt;p&gt;Both fixes shipped this evening. Final confirmation comes tomorrow morning, when the first trading round should show a clean login.&lt;/p&gt;

&lt;h2&gt;
  
  
  A buffer for the trading universe's boundary
&lt;/h2&gt;

&lt;p&gt;Separately, I added a buffer rule to the logic that picks the top-N stocks by market cap as the trading universe.&lt;/p&gt;

&lt;p&gt;Previously, every refresh just swapped in whatever the raw ranking said. That meant a stock sitting right at the boundary could flip in and out of the universe on tiny rank swings — and each flip is unnecessary turnover.&lt;/p&gt;

&lt;p&gt;The fix: stocks already in the universe stay in as long as they don't fall too far past the cutoff, and new stocks only get added once they've clearly climbed past it. The total slot count itself didn't change, so the universe size stays fixed.&lt;/p&gt;

&lt;p&gt;This shipped today for both the KOSPI and KOSDAQ universes, with regression tests passing and the state file confirmed to be generating correctly. Since it just went live, no actual entries or exits have happened yet — I'll get to see the buffer do its job the next time rankings jitter near the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also today
&lt;/h2&gt;

&lt;p&gt;Last night's GPU-detection failure, which caused three back-to-back failures, resolved cleanly: the restarted job finished a full ten-plus-hour analysis run early this morning with no recurrence.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>Qwen3.8-Flash-Next 후기 - RTX 5090 + R9700 두 장으로 돌려본 대형 MoE 모델</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Tue, 08 Sep 2026 00:37:30 +0000</pubDate>
      <link>https://dev.to/finaltype/qwen38-flash-next-hugi-rtx-5090-r9700-du-jangeuro-dolryeobon-daehyeong-moe-model-1d7k</link>
      <guid>https://dev.to/finaltype/qwen38-flash-next-hugi-rtx-5090-r9700-du-jangeuro-dolryeobon-daehyeong-moe-model-1d7k</guid>
      <description>&lt;p&gt;&lt;em&gt;양자화 버전을 세 번 바꾸고 자문을 열여섯 차례 거친 끝에, 프로덕션에 올린 로컬 모델 교체기&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;퀀트봇의 AI 추천 파이프라인에는 &lt;a href="https://finaltype.github.io/quant-blog/architecture/08_ai_ensemble/" rel="noopener noreferrer"&gt;리서치 담당 모델&lt;/a&gt;(새 창)이 있습니다. 뉴스·공시를 사람처럼 읽고 근거를 세워 리포트를 쓰는 역할입니다.&lt;/p&gt;

&lt;p&gt;이번에 그 자리에 새 오픈소스 모델(Qwen3.8-Flash-Next, 전체 파라미터 약 1,250억 개 중 실제로 켜지는 건 약 60억 개뿐인 MoE 구조, Qwen 차기 세대 프리뷰격 모델)을 후보로 올렸습니다. 이번 글은 평소보다 훨씬 구체적으로 씁니다. 판단 로직이 아니라 순수 인프라(어떤 그래픽카드로 어떻게 돌렸는지, 양자화 버전을 정확히 뭘 골랐는지)라서 굳이 감출 이유가 없다고 판단했고, 마침 로컬 LLM을 직접 돌려보는 분들껜 참고가 될 것 같아 과정을 가감없이 남깁니다.&lt;/p&gt;

&lt;p&gt;글이 좀 깁니다. 먼저 결론만 요약하면, 최종적으로 프로덕션에 채택한 건 &lt;strong&gt;Q3_K_XL 양자화&lt;/strong&gt;였고, 오늘 아침(09-08 07:18) 처음으로 100종목을 끊기지 않고 완주한 실제 프로덕션 런의 서버 로그로 환산한 속도는 &lt;strong&gt;프리필 약 610 tok/s, 디코드 약 124 tok/s(4개 슬롯 합산, 슬롯당 약 31 tok/s)&lt;/strong&gt;였습니다.&lt;/p&gt;

&lt;p&gt;아래 본문에는 이 숫자가 나오기까지 시도했던 다른 양자화 후보들, 그리고 동시 요청 수(&lt;code&gt;-np&lt;/code&gt;)·배치 크기(&lt;code&gt;-b&lt;/code&gt;)를 바꿀 때마다 이 프리필·디코드 속도가 어떻게 달라졌는지를 실측 표로 정리해뒀습니다. 그 과정 자체가 궁금하신 분들은 아래를 찬찬히 읽어주세요.&lt;/p&gt;

&lt;h2&gt;
  
  
  목표와 하드웨어
&lt;/h2&gt;

&lt;p&gt;목표는 단순했습니다. 리서치 담당 파이프라인이 한 번에 훑는 종목 100개를 10시간 안에 끝내는 것.&lt;/p&gt;

&lt;p&gt;서빙에 쓴 카드는 두 장입니다. NVIDIA RTX 5090(32GB)과 AMD Radeon AI PRO R9700(32GB), 서로 다른 제조사 카드를 하나의 llama.cpp 서버에 물려서 레이어를 나눠 맡겼습니다. 두 카드 VRAM을 합치면 약 63.6GiB인데, 모델 백본만 해도 그 대부분을 차지하는 크기라 여유가 별로 없었습니다.&lt;/p&gt;

&lt;p&gt;이종 카드를 같이 쓰다 보니 카드마다 다른 문제도 나왔습니다. AMD 카드는 GTT(시스템 메모리를 GPU가 쓰는 영역) 절전 모드로 빠지면 응답이 늦어지는 버그가 있어서, udev 규칙(&lt;code&gt;99-r9700-no-runtime-pm.rules&lt;/code&gt;)으로 런타임 절전 자체를 꺼야 했습니다.&lt;/p&gt;

&lt;p&gt;llama.cpp 기준으로 최종적으로 굳힌 레이어 분배는 &lt;code&gt;-sm layer -ts 23,25&lt;/code&gt;(5090에 23개 층, R9700에 25개 층)였고, 동시 요청 수는 &lt;code&gt;-np 4&lt;/code&gt;, 배치는 &lt;code&gt;-b 512&lt;/code&gt;, 컨텍스트는 &lt;code&gt;-c 131072&lt;/code&gt;였습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  종목당 실제 입출력 토큰량
&lt;/h2&gt;

&lt;p&gt;속도 얘기를 하기 전에, 애초에 이 모델에 뭐가 얼마나 들어가고 나오는지부터 짚고 갑니다. 리서치 담당은 종목 하나를 한 번 훑을 때 여러 단계(뉴스 분석·공시 분석·토론 등)를 거치며 모델을 여러 차례 호출하는데, 그 호출 1건 기준 실측치입니다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;수치&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프롬프트(입력) 평균&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,349 토큰&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;콜 1건 기준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프롬프트(입력) 최대 관측치&lt;/td&gt;
&lt;td&gt;22,129 토큰&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성(출력) 중앙값&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;약 1,205 토큰&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성 상한&lt;/td&gt;
&lt;td&gt;4,096 토큰&lt;/td&gt;
&lt;td&gt;실제로는 거의 도달하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프리필:디코드 토큰량 비율&lt;/td&gt;
&lt;td&gt;약 &lt;strong&gt;6.6배&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;입력이 출력보다 훨씬 큼&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;입력이 출력보다 6.6배 많다는 게 중요한 이유는, 이 워크로드에서 속도를 좌우하는 병목이 "얼마나 빨리 답을 뱉는가"(디코드)보다 "얼마나 빨리 긴 입력을 읽어들이는가"(프리필) 쪽에 더 가깝다는 뜻이기 때문입니다. 아래 동시 요청 수 실험에서 이 프리필 비중이 계속 발목을 잡습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  첫 번째 선택 — 공격적인 양자화로 시작
&lt;/h2&gt;

&lt;p&gt;모델을 통째로 VRAM에 올리려면 GGUF 양자화가 필요했습니다. 처음엔 Unsloth의 UD-IQ3_XXS(용량 약 77~82GB, 그중 전문가(expert) 파라미터 절반은 IQ2_S급 2.5비트 초저정밀도)로 시작했습니다.&lt;/p&gt;

&lt;p&gt;실측해보니 종목당 521초, 100종목이면 14.5시간이었습니다. 목표(10시간)의 1.45배였습니다.&lt;/p&gt;

&lt;p&gt;숫자보다 먼저 걸린 건 "이 정밀도로 판단 품질이 얼마나 깎였는가"였습니다. IQ3_XXS까지 내려가도 되는지 확신이 안 서서, 다른 AI 모델에게 Unsloth가 공개한 양자화 품질 비교표(퍼플렉시티 보존율·KL발산)를 근거로 여러 차례 자문을 구했습니다.&lt;/p&gt;

&lt;p&gt;그 결과 세 후보의 그림이 이렇게 정리됐습니다. Q4_K_XL(품질 보존율 92.3%, KLD 0.047로 가장 높은 정밀도)은 백본만으로도 77GiB라 두 카드 VRAM 64GiB를 넘어서 애초에 후보가 될 수 없었습니다. Q3_K_XL(88.3%, KLD 0.107)이 IQ3_XXS(85.4%, KLD 0.165)보다 품질을 덜 포기하면서도 백본이 57GiB로 VRAM 안에 거의 들어맞았습니다. 결국 처음 받았던 IQ3_XXS를 버리고 Q3_K_XL(용량 약 84GB)을 새로 받았습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  동시성을 올리다 GPU가 통째로 멈추다
&lt;/h2&gt;

&lt;p&gt;속도를 더 끌어올리려고 동시 요청 수를 늘리는 실험을 했습니다. 그런데 특정 조합에서 GPU 자체가 응답을 멈추는 현상이 나왔습니다.&lt;/p&gt;

&lt;p&gt;원인을 두 번 잘못 짚었습니다. 처음엔 소프트웨어 설정 문제로 봤다가, 다시 보니 그것도 아니었고, 결국 커널 로그에 남은 &lt;code&gt;NVRM: Xid 8&lt;/code&gt; — RC(Row Chase) 워치독이 7초 만에 GPU를 강제 리셋한 기록이 진짜 원인이었습니다. GPU가 일정 시간 응답이 없으면 드라이버가 강제로 리셋을 거는 안전장치인데, np=8 동시 요청 조합에서 그 시간을 넘겨버린 겁니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  프롬프트를 줄여서 14%를 벌다
&lt;/h2&gt;

&lt;p&gt;모델 자체를 못 바꾼다면 모델에 넣는 입력을 줄이는 쪽으로 방향을 틀었습니다. 리서치 담당에게 매번 반복해서 넣던 지시문 앞부분을 정리하고, 모델 응답 자체를 더 간결하게 요구하는 패치 두 가지를 적용했습니다.&lt;/p&gt;

&lt;p&gt;같은 조건으로 A/B 비교한 결과 패치 전 457.7초, 패치 후 391.8초로 14.4% 빨라졌습니다. 100종목 기준으로 환산하면 12.7시간에서 10.9시간으로 줄어드는 셈이었습니다.&lt;/p&gt;

&lt;p&gt;패치를 켰다고 판단 품질이 흔들리지는 않았는지도 확인했습니다. 등급이 달라진 종목이 몇 개 나왔지만, 같은 조건을 반복 실행해도 어차피 절반가량은 등급이 흔들리는 모델이라(자기재현율 51.5%), 이 정도 변동은 패치 탓이 아니라 원래 있던 노이즈로 판정했습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  레이어를 다른 카드로 옮겼다가 역전당하다
&lt;/h2&gt;

&lt;p&gt;전문가 파라미터만 골라서 상대적으로 여유 있는 카드로 몰아주는 실험도 해봤습니다. 단일 요청 벤치마크로는 8~17% 빨라 보여서 기대를 걸었습니다.&lt;/p&gt;

&lt;p&gt;그런데 실제 서버 조건(동시 요청 여러 개)으로 다시 재보니 오히려 6.7% 느려졌습니다. 원인을 다시 파보니, 그 배치 방식이 첫 토큰이 나오기까지 걸리는 시간(프리필)을 20%나 늘려버리는 부작용이 있었고, 동시 요청이 여러 개 몰릴 땐 그 부작용이 단일 요청에서 얻은 이득을 통째로 잡아먹었습니다. 이 방식은 폐기했습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  벤치마크 숫자가 거짓말을 한 순간
&lt;/h2&gt;

&lt;p&gt;중간 정밀도 버전을 받은 뒤 표준 벤치마크 도구로 재보니, 프리필 속도가 이전 버전보다 22~25% 나쁘게 나왔습니다. 이대로면 채택을 재검토해야 할 수치였습니다.&lt;/p&gt;

&lt;p&gt;다시 자문을 구해 원인을 파보니, 벤치마크 도구가 모델을 처음 로드할 때 일부 데이터를 지연 로딩하는 방식으로 돌아가고 있었고, 그 첫 실행이 콜드 캐시 상태라 인위적으로 느리게 찍힌 것이었습니다. 실제 조건(캐시가 데워진 상태)으로 교정하면 오히려 2~7% 더 빠른 수치였습니다.&lt;/p&gt;

&lt;p&gt;이 일로 벤치마크 도구 하나의 숫자만 보고 판단하면 안 된다는 걸 다시 확인했습니다. 그래서 이후 모든 판단은 실제 서버를 띄운 상태에서 재현한 값으로만 내리기로 했습니다.&lt;/p&gt;

&lt;p&gt;실서버로 재검증한 결과, 중간 정밀도 버전은 종목당 395.7초로 처음 받았던 버전(391.8초)과 비교해 손해가 1%뿐이었습니다. 압축을 덜 한 버전인데도 속도 손해가 거의 없었으니, 이 버전을 최종 채택했습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  저온 실험이 알려준 것 — 버그가 없었다는 버그
&lt;/h2&gt;

&lt;p&gt;모델 출력을 더 일관되게 만들려고 응답 다양성을 낮추는(온도를 낮추는) 실험도 했습니다. 그런데 다시 자문을 구하는 과정에서 이상한 점이 발견됐습니다.&lt;/p&gt;

&lt;p&gt;실제로는 설정이 서버에 전달되는 배관(파이프라인) 어딘가에서 빠져 있어서, 온도를 낮췄다고 생각한 실행이 사실은 매번 기본값으로 돌아가고 있었습니다. 앞서 의심했던 패치는 죄가 없었던 셈이라, 이 배관 버그를 고치는 작업으로 방향을 돌렸습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  20종목 페어드 비교로 마침표를 찍다
&lt;/h2&gt;

&lt;p&gt;여기까지 오면서 나온 판정들을 마지막으로 한 번에 확인하려고, 패치를 끈 조건과 켠 조건으로 같은 20종목을 각각 통째로 돌려 정면 비교했습니다.&lt;/p&gt;

&lt;p&gt;패치 끈 쪽은 종목당 423.5초, 켠 쪽은 390.5초로 8% 빨랐습니다. 20종목 중 등급이 정확히 일치한 게 11개였고, 나머지 변동에 방향성이 있는지 통계 검정(부호검정)을 걸어봤더니 유의미한 방향성은 없었습니다(p=1.0). 패치가 속도는 올리면서 판단은 흔들지 않는다는 게 이걸로 확정됐습니다.&lt;/p&gt;

&lt;p&gt;최종 구성은 중간 정밀도 양자화에 패치 두 개를 켠 상태로 종목당 390.5초, 100종목 기준 10.85시간. 목표(10시간) 대비 1.085배까지 좁혔습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  프로덕션 전환, 그리고 바로 터진 사고 두 건
&lt;/h2&gt;

&lt;p&gt;여기까지 검증한 결과를 들고 프로덕션 전환을 결정했습니다. 기존에 쓰던 모델을 내리고 이 모델로 상주 서비스 다섯 개를 재시작했습니다.&lt;/p&gt;

&lt;p&gt;전환하자마자 문제 두 가지가 드러났습니다. 첫째, 실행이 끊겼다 이어받는 로직이 "어떤 모델로 만든 결과인지"를 확인하지 않고 있어서, 방금 내린 구모델의 잔재 데이터가 새 모델 라벨을 달고 섞여 들어갈 뻔했습니다. 파라미터 하나를 추가해서 모델이 다르면 이어받지 않도록 막았습니다.&lt;/p&gt;

&lt;p&gt;둘째, 문제가 생기면 예전 모델로 자동으로 되돌아가는 장치도, 자동 백업도 없다는 걸 그제야 발견했습니다. 일단 수동으로 백업부터 해두고 안전망 스크립트를 급하게 만들어 덮었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  전환 첫날 밤, 완전히 무관한 원인으로 스킵
&lt;/h2&gt;

&lt;p&gt;전환 당일 밤은 100종목 중 77개만 처리하고 나머지를 건너뛰었습니다. 새 모델 탓이라고 의심하며 로그를 뒤졌는데, 원인은 전혀 다른 곳이었습니다.&lt;/p&gt;

&lt;p&gt;코드 리뷰용으로 짜둔 테스트 스크립트 하나가 정리 과정에서 "8090번 포트를 쓰는 llama-server 프로세스를 전부 종료"하는 명령을 실행했는데, 하필 그 포트가 방금 올린 프로덕션 서버 포트와 같았습니다. 테스트가 실제 서비스를 오살한 겁니다.&lt;/p&gt;

&lt;p&gt;서버가 34분간 응답이 없자 시스템은 이걸 외부 네트워크 장애로 오판했고, 그 오판이 이어져 나머지 종목들을 건너뛰는 결과로 이어졌습니다. 테스트 스크립트에 안전장치(가짜 응답으로 대체하지 않는 한 진짜 포트를 건드리지 못하게 막는 가드)를 추가해서 막았습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  이튿날 — 동시 요청 수(np)·배치(-b) 조합을 하루 종일 훑다
&lt;/h2&gt;

&lt;p&gt;프로덕션 전환 다음 날은 하루를 통째로 써서 동시 요청 수(&lt;code&gt;-np&lt;/code&gt;)와 배치 크기(&lt;code&gt;-b&lt;/code&gt;), 두 카드 간 레이어 분배(&lt;code&gt;-ts&lt;/code&gt;)를 조합별로 바꿔가며 실측했습니다. 그동안 실서버로 재확인한 조합만 추리면 이렇습니다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조합&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;th&gt;종목당 소요(100종목 환산)&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;np=1(초기 기준)&lt;/td&gt;
&lt;td&gt;성공&lt;/td&gt;
&lt;td&gt;524초(14.5시간)&lt;/td&gt;
&lt;td&gt;동시 요청 없음 — 프리필 68초 + 디코드 316초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;np=4, -b512, ts23/25(최종 채택)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;성공&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;391.8초(10.88시간, 검증 실행 기준)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;패치 켠 상태. 아래 "결과" 절의 오늘 실제 100종목 완주는 종목당 369.9초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=4, -b1024, ts23/25&lt;/td&gt;
&lt;td&gt;성공하지만 더 느림&lt;/td&gt;
&lt;td&gt;472.0초(13.1시간)&lt;/td&gt;
&lt;td&gt;배치를 올려도 이득 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=5, -b512, ts23/25(&lt;code&gt;-c&lt;/code&gt; 133,120으로 확장)&lt;/td&gt;
&lt;td&gt;성공하지만 더 느림&lt;/td&gt;
&lt;td&gt;475.0초(13.2시간)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=5, -b1024, ts23/25(&lt;code&gt;-c&lt;/code&gt; 133,120)&lt;/td&gt;
&lt;td&gt;성공&lt;/td&gt;
&lt;td&gt;442.3초(12.3시간)&lt;/td&gt;
&lt;td&gt;np=4보다 느려서 미채택 — 슬롯당 남는 컨텍스트 여유가 0이 되는 구성이라 완주 안정성 관점에서도 보류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=8, &lt;code&gt;-c&lt;/code&gt; 160,000(통합 KV)&lt;/td&gt;
&lt;td&gt;겉보기엔 진행 중, 실제론 71분 만에 GPU 강제 리셋&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;커널 로그의 &lt;code&gt;Xid 8&lt;/code&gt; — 응답 없음 워치독이 GPU를 리셋한 기록으로 뒤늦게 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=4, &lt;code&gt;-c&lt;/code&gt; 160,000(통합 KV)&lt;/td&gt;
&lt;td&gt;기동 4.5분 만에 같은 GPU 강제 리셋&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=3, -b1024, ts23/25(검증된 "안전한" 분배)&lt;/td&gt;
&lt;td&gt;GPU 메모리 부족(OOM)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;로드 자체가 안 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=3 / np=4, ts24/24&lt;/td&gt;
&lt;td&gt;둘 다 GPU 메모리 부족(OOM)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;레이어 분배 자체가 근본 한계로 최종 기각(np값과 무관)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;몇 가지 되짚어볼 지점이 있었습니다.&lt;/p&gt;

&lt;p&gt;처음엔 배치 크기(&lt;code&gt;-b 1024&lt;/code&gt;)를 OOM의 원인으로 의심했습니다. np=3/np=5 조합에서 OOM이 반복됐거든요. 그런데 다시 짚어보니 &lt;code&gt;-b&lt;/code&gt; 값은 GPU가 한 번에 묶어 처리하는 토큰 수만 바꿀 뿐, 메모리 예약량 자체는 별도 설정(&lt;code&gt;-ub&lt;/code&gt;, 이번엔 지정 안 함)이 정해서 &lt;code&gt;-b&lt;/code&gt;와 무관하다는 게 드러났습니다. 애먼 변수를 의심했던 셈입니다.&lt;/p&gt;

&lt;p&gt;더 이상한 건 np=5는 성공했는데 똑같은 설정(&lt;code&gt;-c&lt;/code&gt; 133,120, ts23/25)에서 np=3은 실패했다는 점이었습니다. 동시 요청을 더 적게 시켰는데 오히려 메모리가 모자란 역설적인 결과라, 그날 실험만으로는 원인을 확정하지 못하고 "그 시점 GPU에 남아있던 여유 메모리가 우연히 달랐다"는 가설과 "슬롯 수가 적을수록 슬롯 하나가 차지하는 버퍼가 커진다"는 가설 두 개만 남긴 채 미해결로 넘겼습니다. 더 파고들 실익(3% 안팎)보다 다른 우선순위가 급했기 때문입니다.&lt;/p&gt;

&lt;p&gt;이 과정에서 눈에 띈 관측도 하나 있었습니다. AMD 카드는 거의 항상 사용률 100%로 병목이었고, NVIDIA 카드는 오히려 17~26%는 놀고 있었습니다. 카드 두 장의 성능 격차가 그대로 병목으로 드러난 셈입니다.&lt;/p&gt;

&lt;p&gt;결국 첫날 정한 &lt;code&gt;-np 4 -b 512 -c 131072 ts23/25&lt;/code&gt; 조합을 그대로 굳히기로 했습니다. 다른 조합 중 이걸 이긴 게 없었고, 프로덕션 전환 후 첫 완주라는 목표 앞에서 굳이 불안정한 조합(np=5의 컨텍스트 마진 0)으로 바꿀 이유가 없었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  결과 — 오늘 아침 완주한 실제 데이터, 그리고 아직 답하지 못한 질문
&lt;/h2&gt;

&lt;p&gt;전환 이틀째 되던 날 아침(2026-09-08 07:18), 전날 21:02에 시작해 10시간 17분 만에 처음으로 100종목을 끊기지 않고 완주했습니다. 테스트가 아니라 이 실제 런의 서버 로그를 직접 파싱해서 낸 수치입니다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;수치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;종목 수&lt;/td&gt;
&lt;td&gt;100개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 시각&lt;/td&gt;
&lt;td&gt;09-07 21:02 ~ 09-08 07:18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;총 소요&lt;/td&gt;
&lt;td&gt;10시간 17분(종목당 평균 369.9초)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버가 실제로 처리한 프롬프트 토큰&lt;/td&gt;
&lt;td&gt;11,658,471&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성한 토큰&lt;/td&gt;
&lt;td&gt;2,215,506&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프리필 속도&lt;/td&gt;
&lt;td&gt;약 610 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디코드 속도&lt;/td&gt;
&lt;td&gt;약 124 tok/s(4슬롯 합산, 슬롯당 약 31 tok/s)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;종목당 369.9초는 앞서 검증 단계에서 봤던 390~396초대 숫자보다도 더 빠른 값입니다. 검증은 몇 종목·몇십 종목짜리 표본이었고, 이건 100종목 전체를 실제로 돌린 값이라 차이가 나는 게 자연스럽습니다.&lt;/p&gt;

&lt;p&gt;속도 튜닝이라는 목표 자체는 여기서 일단락됐습니다.&lt;/p&gt;

&lt;p&gt;다만 아직 확인 못 한 게 하나 있습니다. 이 모델이 실제로 좋은 판단을 내리는지, 즉 진짜 매매 성과 기준의 품질은 이 시점까지 단 한 번도 측정하지 않았습니다. 여기서 다룬 건 전부 "얼마나 빠른가"와 "패치가 판단을 안 흔드는가"뿐이고, "판단 자체가 얼마나 좋은가"는 앞으로 따로 재봐야 할 숙제로 남아 있습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  돌아보며
&lt;/h2&gt;

&lt;p&gt;이번 과정에서 가장 크게 남은 교훈은, 벤치마크 도구 하나의 숫자를 그대로 믿지 않는 습관이었습니다. 도구가 콜드 캐시 상태를 재고 있었다는 것도, 온도 설정이 배관 어딘가에서 새고 있었다는 것도, 전부 "숫자가 이상하다"는 감각 하나로 자문을 다시 구하고 나서야 드러났습니다.&lt;/p&gt;

&lt;p&gt;전에 &lt;a href="https://finaltype.github.io/quant-blog/postmortems/2026-08-22_hardware-verdict-reversal/" rel="noopener noreferrer"&gt;그래픽카드를 교체하면서 벤치마크를 잘못 읽어 판정이 두 번 뒤집혔던 일&lt;/a&gt;(새 창)을 정리한 적이 있는데, 이번에도 같은 교훈이 반복됐습니다. 카탈로그 숫자나 단발성 벤치마크보다, 실제로 쓸 조건 그대로 재현해서 얻은 실측값이 항상 더 믿을 만했습니다.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>Qwen3.8-Flash-Next Review - Running It on an RTX 5090 + R9700 Pair</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Tue, 08 Sep 2026 00:37:29 +0000</pubDate>
      <link>https://dev.to/finaltype/qwen38-flash-next-review-running-it-on-an-rtx-5090-r9700-pair-4pg2</link>
      <guid>https://dev.to/finaltype/qwen38-flash-next-review-running-it-on-an-rtx-5090-r9700-pair-4pg2</guid>
      <description>&lt;p&gt;&lt;em&gt;Three quantization swaps and sixteen rounds of advice later, the story of a local model swap that shipped to production&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The AI recommendation pipeline in my trading bot has a &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/08_ai_ensemble/" rel="noopener noreferrer"&gt;research model&lt;/a&gt;(new tab). Its job is to read news and disclosures the way a person would, then write a report backed by reasoning.&lt;/p&gt;

&lt;p&gt;I recently put a new open-source model up as a candidate for that role: Qwen3.8-Flash-Next, an MoE model with roughly 125 billion total parameters but only about 6 billion actually active per token, positioned as a preview of Qwen's next generation. This post is far more specific than usual. What I'm describing here is pure infrastructure — which GPUs, how they were configured, exactly which quantization I picked — not the decision logic itself, so there's no real reason to hold it back. It should also be useful reference for anyone running local LLMs themselves, so I'm leaving the process in unfiltered.&lt;/p&gt;

&lt;p&gt;This post runs long, so here's the short version up front: the quantization I ultimately shipped to production was &lt;strong&gt;Q3_K_XL&lt;/strong&gt;. This morning (09-08, 07:18) the pipeline finished all 100 tickers without a skip for the first time, and the server log from that actual run works out to roughly &lt;strong&gt;610 tok/s prefill and 124 tok/s decode&lt;/strong&gt; (summed across 4 concurrent slots, about 31 tok/s per slot).&lt;/p&gt;

&lt;p&gt;The rest of the post walks through the other quantization candidates I tried before landing on that, and a measured table of how prefill/decode speed shifted every time I changed the concurrency count (&lt;code&gt;-np&lt;/code&gt;) or batch size (&lt;code&gt;-b&lt;/code&gt;). If you're curious about that process, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal and hardware
&lt;/h2&gt;

&lt;p&gt;The goal was simple: get the research pipeline through its usual sweep of 100 tickers in under 10 hours.&lt;/p&gt;

&lt;p&gt;Serving ran on two cards: an NVIDIA RTX 5090 (32GB) and an AMD Radeon AI PRO R9700 (32GB), two different vendors' cards split across layers on a single llama.cpp server. Combined VRAM was about 63.6GiB, and the model backbone alone took up most of that — there wasn't much room to spare.&lt;/p&gt;

&lt;p&gt;Mixing vendors brought its own quirks. The AMD card had a bug where dropping into GTT (system memory the GPU borrows) power-saving mode slowed responses down, so I had to disable runtime power management entirely via a udev rule (&lt;code&gt;99-r9700-no-runtime-pm.rules&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The layer split I eventually settled on was &lt;code&gt;-sm layer -ts 23,25&lt;/code&gt; (23 layers on the 5090, 25 on the R9700), with &lt;code&gt;-np 4&lt;/code&gt; concurrent requests, batch size &lt;code&gt;-b 512&lt;/code&gt;, and context &lt;code&gt;-c 131072&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actual input/output tokens per ticker
&lt;/h2&gt;

&lt;p&gt;Before getting into speed, it's worth pinning down what actually goes in and out of this model. Sweeping one ticker takes several stages (news analysis, disclosure analysis, debate, and so on), each triggering its own model call — these are the measured figures per individual call.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg prompt (input) size&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,349 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;per call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max observed prompt (input) size&lt;/td&gt;
&lt;td&gt;22,129 tokens&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median generation (output) size&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1,205 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation ceiling&lt;/td&gt;
&lt;td&gt;4,096 tokens&lt;/td&gt;
&lt;td&gt;rarely actually reached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefill:decode token ratio&lt;/td&gt;
&lt;td&gt;about &lt;strong&gt;6.6x&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;input dwarfs output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 6.6x gap matters because it means the real bottleneck in this workload leans more toward "how fast can it read a long input" (prefill) than "how fast can it generate an answer" (decode). That prefill weight keeps showing up as the limiting factor in the concurrency experiments below.&lt;/p&gt;

&lt;h2&gt;
  
  
  First choice - starting aggressive on quantization
&lt;/h2&gt;

&lt;p&gt;Fitting the whole model into VRAM meant GGUF quantization. I started with Unsloth's UD-IQ3_XXS (about 77-82GB, with half the expert parameters down at IQ2_S-grade 2.5-bit precision).&lt;/p&gt;

&lt;p&gt;Measured throughput came out to 521 seconds per ticker — 14.5 hours for 100 tickers, 1.45x the 10-hour target.&lt;/p&gt;

&lt;p&gt;Before the raw numbers, what actually bothered me was "how much judgment quality did this precision cost." I wasn't confident IQ3_XXS was safe to go with, so I asked for advice from another AI model several times, using Unsloth's published quantization quality comparisons (perplexity retention, KL divergence) as the basis.&lt;/p&gt;

&lt;p&gt;That settled the picture across three candidates. Q4_K_XL (92.3% quality retention, KLD 0.047, the highest precision) needed 77GiB for the backbone alone — over the combined 64GiB VRAM budget, so it was disqualified outright. Q3_K_XL (88.3%, KLD 0.107) gave up less quality than IQ3_XXS (85.4%, KLD 0.165) while its 57GiB backbone nearly fit inside VRAM. I dropped the original IQ3_XXS and pulled down Q3_K_XL instead (about 84GB).&lt;/p&gt;

&lt;h2&gt;
  
  
  Raising concurrency crashed the GPU outright
&lt;/h2&gt;

&lt;p&gt;Chasing more speed, I tried raising the number of concurrent requests. At a certain combination, the GPU itself simply stopped responding.&lt;/p&gt;

&lt;p&gt;I misdiagnosed the cause twice. First I blamed a software setting, then ruled that out too. The real cause turned out to be sitting in the kernel log the whole time: &lt;code&gt;NVRM: Xid 8&lt;/code&gt; — the RC (Row Chase) watchdog forcibly resetting the GPU after 7 seconds. That's a safety mechanism that force-resets the GPU when it goes unresponsive too long, and the &lt;code&gt;np=8&lt;/code&gt; concurrency combination pushed past that window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shrinking the prompt bought 14%
&lt;/h2&gt;

&lt;p&gt;If the model itself couldn't change, the next lever was shrinking what got fed into it. I cleaned up the boilerplate at the front of the instructions repeated on every research call, and applied a second patch asking the model for more concise output.&lt;/p&gt;

&lt;p&gt;An A/B comparison under identical conditions came out to 457.7 seconds before the patches, 391.8 seconds after — 14.4% faster. Projected across 100 tickers, that's a drop from 12.7 hours to 10.9 hours.&lt;/p&gt;

&lt;p&gt;I also checked whether turning the patches on shook judgment quality. A handful of tickers did change grade, but this model already flips grades on roughly half its calls even on repeated identical runs (self-reproducibility of 51.5%), so I ruled this variation as pre-existing noise rather than something the patches caused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving layers to the other card backfired
&lt;/h2&gt;

&lt;p&gt;I also tried routing expert-layer parameters preferentially onto whichever card had more headroom. Single-request benchmarks looked promising — 8-17% faster.&lt;/p&gt;

&lt;p&gt;But retested under real server conditions (multiple concurrent requests), it came out 6.7% slower instead. Digging further, that batching approach turned out to inflate time-to-first-token (prefill) by 20%, and once several concurrent requests piled up, that side effect ate the entire gain the single-request benchmark had shown. I dropped this approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment the benchmark numbers lied
&lt;/h2&gt;

&lt;p&gt;After receiving the mid-precision version, a standard benchmarking tool measured prefill speed as 22-25% worse than the previous version — a number bad enough to force reconsidering the whole choice.&lt;/p&gt;

&lt;p&gt;Asking for advice again to dig into the cause, it turned out the benchmark tool lazy-loads part of its data on the model's first run, and that cold-cache first pass was what artificially tanked the number. Corrected for real conditions (a warmed cache), the version was actually 2-7% faster.&lt;/p&gt;

&lt;p&gt;That reinforced a lesson: never trust a single benchmark tool's number at face value. From then on, every judgment was made only from values reproduced with an actual running server.&lt;/p&gt;

&lt;p&gt;Re-verified on the real server, the mid-precision version came out to 395.7 seconds per ticker — only a 1% loss versus the first version's 391.8 seconds. Given how much less it had been compressed, that near-zero speed cost made it the version I ultimately adopted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the low-temperature experiment revealed - a bug that wasn't there
&lt;/h2&gt;

&lt;p&gt;I also tried lowering output diversity (temperature) to make model output more consistent. But while asking for advice on this, something strange surfaced.&lt;/p&gt;

&lt;p&gt;The setting was actually getting dropped somewhere in the plumbing that passes configuration to the server, so every run I thought had lowered the temperature had actually silently fallen back to default the whole time. The patch I'd suspected earlier turned out to be innocent, so I redirected the work toward fixing this plumbing bug instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop with a 20-ticker paired comparison
&lt;/h2&gt;

&lt;p&gt;To settle every judgment made up to this point in one pass, I ran the same 20 tickers end-to-end with the patches off and with them on, head to head.&lt;/p&gt;

&lt;p&gt;Patches off came out to 423.5 seconds per ticker, patches on to 390.5 seconds — 8% faster. Grades matched exactly on 11 of the 20 tickers, and a sign test on the remaining differences found no significant directionality (p=1.0). That confirmed the patches raise speed without shaking judgment.&lt;/p&gt;

&lt;p&gt;The final configuration — mid-precision quantization with both patches on — landed at 390.5 seconds per ticker, 10.85 hours for 100 tickers. That's 1.085x the 10-hour target, as close as I got it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cutting over to production, and two incidents right away
&lt;/h2&gt;

&lt;p&gt;With everything validated up to this point, I decided to cut over to production — took down the previous model and restarted five resident services onto this one.&lt;/p&gt;

&lt;p&gt;Two problems surfaced the moment it went live. First, the logic that resumes an interrupted run wasn't checking which model had produced the existing results, so leftover data from the just-retired model was about to get mixed in under the new model's label. I added a parameter to block resuming whenever the model doesn't match.&lt;/p&gt;

&lt;p&gt;Second, I only discovered at that point that there was no automatic fallback to the previous model if something went wrong, and no automatic backup either. I manually backed things up first, then hastily built a safety-net script to cover the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Night one after the cutover, skipped for a completely unrelated reason
&lt;/h2&gt;

&lt;p&gt;The first night after cutover, only 77 of 100 tickers got processed; the rest were skipped. I suspected the new model and dug through logs, but the actual cause was somewhere else entirely.&lt;/p&gt;

&lt;p&gt;A test script written for code review purposes had, during cleanup, run a command to kill every &lt;code&gt;llama-server&lt;/code&gt; process on port 8090 — which happened to be the exact port the freshly-deployed production server was using. The test had killed the live service.&lt;/p&gt;

&lt;p&gt;With the server unresponsive for 34 minutes, the system misread it as an external network failure, and that misdiagnosis cascaded into skipping the remaining tickers. I added a safeguard to the test script (a guard that blocks it from touching a real port unless it's substituting a mock response).&lt;/p&gt;

&lt;h2&gt;
  
  
  The next day - sweeping concurrency (np) and batch size (-b) all day
&lt;/h2&gt;

&lt;p&gt;The day after cutover, I spent the whole day sweeping combinations of concurrency (&lt;code&gt;-np&lt;/code&gt;), batch size (&lt;code&gt;-b&lt;/code&gt;), and the layer split between the two cards (&lt;code&gt;-ts&lt;/code&gt;). Here are the combinations actually re-verified on the real server.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Combination&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Per-ticker time (100-ticker projection)&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;np=1 (initial baseline)&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;524s (14.5h)&lt;/td&gt;
&lt;td&gt;No concurrency — 68s prefill + 316s decode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;np=4, -b512, ts23/25 (final adopted)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;391.8s (10.88h, from validation runs)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Patches on. Today's actual 100-ticker completion (see "Result" below) came in at 369.9s/ticker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=4, -b1024, ts23/25&lt;/td&gt;
&lt;td&gt;Success, but slower&lt;/td&gt;
&lt;td&gt;472.0s (13.1h)&lt;/td&gt;
&lt;td&gt;Raising batch gave no benefit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=5, -b512, ts23/25 (&lt;code&gt;-c&lt;/code&gt; extended to 133,120)&lt;/td&gt;
&lt;td&gt;Success, but slower&lt;/td&gt;
&lt;td&gt;475.0s (13.2h)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=5, -b1024, ts23/25 (&lt;code&gt;-c&lt;/code&gt; 133,120)&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;442.3s (12.3h)&lt;/td&gt;
&lt;td&gt;Slower than np=4, not adopted — also zeroes out per-slot context margin, a concern for completion stability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=8, &lt;code&gt;-c&lt;/code&gt; 160,000 (unified KV)&lt;/td&gt;
&lt;td&gt;Looked like it was still running; GPU force-reset at 71 minutes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Only confirmed later via kernel log &lt;code&gt;Xid 8&lt;/code&gt; — the unresponsive-watchdog reset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=4, &lt;code&gt;-c&lt;/code&gt; 160,000 (unified KV)&lt;/td&gt;
&lt;td&gt;Same GPU force-reset, 4.5 minutes into startup&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=3, -b1024, ts23/25 (the verified "safe" split)&lt;/td&gt;
&lt;td&gt;Out of GPU memory (OOM)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Failed to even load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;np=3 / np=4, ts24/24&lt;/td&gt;
&lt;td&gt;Both out of GPU memory (OOM)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;This layer split itself was a fundamental limit, rejected regardless of np&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things worth revisiting from this sweep.&lt;/p&gt;

&lt;p&gt;I first suspected batch size (&lt;code&gt;-b 1024&lt;/code&gt;) as the cause of the OOMs, since they kept showing up on np=3/np=5 runs. Digging back in, though, &lt;code&gt;-b&lt;/code&gt; only changes how many tokens the GPU batches per pass — the memory reservation itself is governed by a separate setting (&lt;code&gt;-ub&lt;/code&gt;, left unset here), independent of &lt;code&gt;-b&lt;/code&gt;. I'd been suspecting the wrong variable.&lt;/p&gt;

&lt;p&gt;Odder still: np=5 succeeded on the exact same settings (&lt;code&gt;-c&lt;/code&gt; 133,120, ts23/25) where np=3 failed. Fewer concurrent requests running short on memory is the opposite of what you'd expect. I couldn't pin down the cause from that day's experiments alone, and left it unresolved with two competing hypotheses — "the GPU happened to have less free memory available at that particular moment" versus "fewer slots means each slot's buffer grows larger" — since chasing it further (worth maybe 3%) wasn't worth the priority over other things.&lt;/p&gt;

&lt;p&gt;One observation stood out along the way: the AMD card was almost always pinned at 100% utilization and was the bottleneck, while the NVIDIA card sat idle 17-26% of the time. The performance gap between the two cards showed up directly as the bottleneck.&lt;/p&gt;

&lt;p&gt;In the end, I locked in the combination settled on day one — &lt;code&gt;-np 4 -b 512 -c 131072 ts23/25&lt;/code&gt;. Nothing else beat it, and with the first full completion after cutover as the goal, there was no reason to switch to a less stable combination (np=5's zeroed-out context margin).&lt;/p&gt;

&lt;h2&gt;
  
  
  The result - today's actual completion data, and a question still unanswered
&lt;/h2&gt;

&lt;p&gt;On the second morning after cutover (2026-09-08, 07:18), the run that started the previous evening at 21:02 finished all 100 tickers without a single skip for the first time — 10 hours 17 minutes end to end. These numbers come from directly parsing that actual run's server log, not a test.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tickers&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run window&lt;/td&gt;
&lt;td&gt;09-07 21:02 to 09-08 07:18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total time&lt;/td&gt;
&lt;td&gt;10h 17m (avg 369.9s/ticker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt tokens actually processed by the server&lt;/td&gt;
&lt;td&gt;11,658,471&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated tokens&lt;/td&gt;
&lt;td&gt;2,215,506&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefill speed&lt;/td&gt;
&lt;td&gt;~610 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decode speed&lt;/td&gt;
&lt;td&gt;~124 tok/s (summed across 4 slots, ~31 tok/s per slot)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;369.9s/ticker is even faster than the 390-396s range seen during validation. Validation ran on samples of a few to a few dozen tickers, so some difference from a genuine full 100-ticker run is expected.&lt;/p&gt;

&lt;p&gt;The speed-tuning goal itself was settled there.&lt;/p&gt;

&lt;p&gt;One thing I still haven't checked, though: whether this model actually makes good calls — quality measured against real trading performance — hasn't been measured even once up to this point. Everything covered here was about "how fast" and "whether the patches shake judgment," never "how good is the judgment itself." That's still homework left for later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking back
&lt;/h2&gt;

&lt;p&gt;The biggest lesson to come out of this was the habit of never trusting a single benchmark tool's number outright. Both the fact that the tool was measuring a cold cache and the fact that the temperature setting was leaking somewhere in the plumbing only surfaced because a number "felt off" enough to go ask for advice again.&lt;/p&gt;

&lt;p&gt;I've &lt;a href="https://finaltype.github.io/quant-blog/en/postmortems/2026-08-22_hardware-verdict-reversal/" rel="noopener noreferrer"&gt;written before&lt;/a&gt;(new tab) about a time a hardware swap and a misread benchmark flipped a verdict twice, and the same lesson repeated here. A real value reproduced under the exact conditions you'll actually use was always more trustworthy than catalog numbers or a one-off benchmark.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>"[260907] VRAM 정리 패치가 저녁 프로덕션 장애로 되돌아온 하루"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:03:23 +0000</pubDate>
      <link>https://dev.to/finaltype/260907-vram-jeongri-paeciga-jeonyeog-peurodeogsyeon-jangaero-doedolaon-haru-3lph</link>
      <guid>https://dev.to/finaltype/260907-vram-jeongri-paeciga-jeonyeog-peurodeogsyeon-jangaero-doedolaon-haru-3lph</guid>
      <description>&lt;p&gt;&lt;em&gt;낮에 적용한 GPU 메모리 정리 패치가 저녁 프로덕션 3연속 실패로 돌아왔고, 별도로 증권사 API 페이지네이션 버그도 하나 잡았습니다&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  VRAM 정리 패치가 낳은 저녁 장애
&lt;/h2&gt;

&lt;p&gt;낮에는 신호모델 하나가 그래픽카드 메모리를 계속 조금씩 붙잡고 있는 문제를 정리했습니다.&lt;/p&gt;

&lt;p&gt;이 모델은 원래도 계산량이 작아 그래픽카드를 꼭 써야 할 이유가 없었는데, 종료 후에도 메모리 일부를 반납하지 않고 남기고 있었습니다. 코드 레벨에서 이 모델을 아예 CPU로만 돌게 고정하고, 그래도 남는 잔여분은 실행 단위 설정에 "이 그래픽카드는 아예 안 보이게" 하는 값을 추가해서 완전히 비웠습니다.&lt;/p&gt;

&lt;p&gt;문제는 저녁에 터졌습니다. 이 설정값이 원래 목표였던 신호모델뿐 아니라, 같은 실행 단위 안에서 별도로 뜨는 대형 언어모델 서버 프로세스에까지 그대로 상속됐습니다.&lt;/p&gt;

&lt;p&gt;대형 언어모델 서버 입장에서는 자기가 써야 할 그래픽카드가 갑자기 안 보이게 된 셈이라, 기동 자체가 실패했습니다. 이어서 예비 경로로 넘어갔는데 그 예비 모델마저 설치가 안 돼 있어서, 세 번 연속 빠르게 실패했습니다.&lt;/p&gt;

&lt;p&gt;다행히 이 상황을 위해 미리 만들어 둔 안전장치가 정상 작동했습니다. 짧은 시간 안에 연속 실패가 쌓이면 그날 작업을 종목 0개로 안전하게 종료시키는 장치인데, 덕분에 가짜 데이터가 쌓이는 일 없이 조용히 멈췄습니다.&lt;/p&gt;

&lt;p&gt;원인을 찾은 뒤에는 문제의 설정값을 실행 단위 전체가 아니라 신호모델 쪽 코드에서만 걸도록 좁혀 다시 적용했습니다. 재시작 후 대형 언어모델 서버가 정상적으로 그래픽카드를 다시 찾았고, 그날의 종목 분석도 이어서 정상적으로 시작됐습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  증권사 API 페이지네이션 버그
&lt;/h2&gt;

&lt;p&gt;모의투자 계좌에서 보유 종목 하나가 조회 결과에서 통째로 빠지는 문제도 있었습니다.&lt;/p&gt;

&lt;p&gt;내부 장부와 실제 체결 기록에는 둘 다 정상적으로 남아있는데, 증권사 API로 잔고를 물어보면 그 종목만 없는 것으로 나왔습니다. 자동매매 로직의 &lt;a href="https://finaltype.github.io/quant-blog/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;장부대조 안전장치&lt;/a&gt;(새 창)가 이걸 "장부와 실제 잔고가 어긋났다"고 판단해서 해당 종목 거래를 안전하게 멈췄습니다.&lt;/p&gt;

&lt;p&gt;원인은 페이지네이션이었습니다. 증권사 API는 잔고가 많으면 여러 페이지로 나눠서 응답하는데, 다음 페이지가 더 있다는 표시를 응답 헤더로 알려줍니다.&lt;/p&gt;

&lt;p&gt;기존 코드는 첫 페이지만 읽고 끝내고 있었습니다. 문제가 된 종목이 하필 두 번째 페이지에 있어서, 별다른 오류 없이 조용히 누락되고 있었습니다.&lt;/p&gt;

&lt;p&gt;다음 페이지 존재 여부를 확인해서 끝까지 이어붙이도록 고쳤고, 이후 라운드에서 해당 종목이 다시 정상적으로 인식되는 것을 확인했습니다. 비슷한 페이지네이션 구조를 쓰는 다른 API 호출들도 같은 문제가 있을 수 있어서, 필요하면 나중에 따로 점검하기로 남겨뒀습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  그 외 - 장중 관측 서버 헬스체크 버그
&lt;/h2&gt;

&lt;p&gt;낮 동안 장중에 별도로 돌리는 관측용 언어모델 서버 하나가 매번 시간을 다 채우고서야 겨우 기동에 성공하는 것처럼 보이는 문제도 있었습니다.&lt;/p&gt;

&lt;p&gt;들여다보니 이 서버가 실제로는 80초 안팎이면 이미 정상 기동을 마치는데, 기동 확인 코드가 응답 형식을 잘못 가정하고 있어서 매번 정상 응답도 실패로 잘못 읽고 있었습니다. 그 결과 실제로는 멀쩡한 서버를 앞에 두고도 매번 최대 대기시간을 다 채운 뒤에야 넘어가고 있었던 것으로 보입니다.&lt;/p&gt;

&lt;p&gt;확인 방식을 다른 서버들과 같은 기준으로 맞춘 뒤로는 기동 확인이 정상 속도로 통과하는 것을 확인했습니다.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[Sep 7] A VRAM Cleanup Patch Came Back as an Evening Production Outage"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:02:22 +0000</pubDate>
      <link>https://dev.to/finaltype/sep-7-a-vram-cleanup-patch-came-back-as-an-evening-production-outage-omp</link>
      <guid>https://dev.to/finaltype/sep-7-a-vram-cleanup-patch-came-back-as-an-evening-production-outage-omp</guid>
      <description>&lt;p&gt;&lt;em&gt;A GPU memory cleanup applied earlier in the day triggered three straight production failures that evening, and I also fixed a brokerage API pagination bug&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The VRAM cleanup patch that caused an evening outage
&lt;/h2&gt;

&lt;p&gt;Earlier in the day I cleaned up an issue where one of the signal models kept holding onto a small amount of graphics card memory.&lt;/p&gt;

&lt;p&gt;This model never really needed the GPU in the first place — its compute load is small — but it wasn't releasing all of its memory even after finishing. I pinned it to run on CPU only at the code level, and to clear the last remaining bit, added a setting to its service unit that made that graphics card invisible to it entirely.&lt;/p&gt;

&lt;p&gt;The trouble started that evening. That setting didn't just apply to the signal model I was targeting — it also got inherited by a separate large language model server process running under the same service unit.&lt;/p&gt;

&lt;p&gt;From that server's point of view, the graphics card it needed had suddenly vanished, so it failed to start. It fell back to a backup path, but the backup model wasn't installed either, so it failed three times in a row, quickly.&lt;/p&gt;

&lt;p&gt;Fortunately, a safety mechanism built for exactly this kind of situation kicked in. When failures pile up within a short window, it safely stops that day's run with zero tickers processed — so no bad data got written, it just quietly stopped.&lt;/p&gt;

&lt;p&gt;Once I traced the cause, I narrowed the fix so the setting only applies inside the signal model's own code, not the whole service unit. After restarting, the language model server found its graphics card again normally, and that day's ticker analysis picked back up as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  A brokerage API pagination bug
&lt;/h2&gt;

&lt;p&gt;A separate issue: one held position was silently missing from a paper-trading account's holdings query.&lt;/p&gt;

&lt;p&gt;Both the internal ledger and the actual trade fill records showed the position correctly, but the brokerage API's balance query came back without it. The trading logic's &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;ledger-reconciliation safety check&lt;/a&gt;(new tab) flagged the mismatch and safely halted trading on that ticker.&lt;/p&gt;

&lt;p&gt;The root cause was pagination. The brokerage API splits large balances across multiple pages and signals whether more pages remain via a response header.&lt;/p&gt;

&lt;p&gt;The existing code only ever read the first page. The missing position happened to be sitting on the second page, so it dropped out silently with no error.&lt;/p&gt;

&lt;p&gt;I fixed it to check for and follow the next-page flag until the response was exhausted, and confirmed the position was recognized correctly again on the next round. Other API calls using the same pagination pattern might have the same gap — flagged for a separate check later if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also today — an intraday shadow server healthcheck bug
&lt;/h2&gt;

&lt;p&gt;A separate observation-only language model server that runs during market hours always seemed to take the maximum wait time just to report itself as ready.&lt;/p&gt;

&lt;p&gt;It turned out the server was actually finishing startup in under 90 seconds — the healthcheck code was just assuming a response format it never sent, so it misread every healthy response as a failure. As a result, a perfectly healthy server was waiting out its full timeout every single time before the code moved on.&lt;/p&gt;

&lt;p&gt;After aligning the check with the same logic used by the other servers, startup confirmation now passes at normal speed.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
  </channel>
</rss>
