自动任务按推理、量化因子、微调轮流生成并派发。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-11 09:15:36 +08:00
parent 29551a260a
commit 8f562d00a0
4 changed files with 198 additions and 51 deletions
+164 -41
View File
@@ -1,6 +1,7 @@
"""量化因子挖掘的排队与自动调度。
"""自动任务排队。
没有待领取的 alpha_mining 任务时补一批;距上一批已满 2 小时时再补一批。
大模型推理、量化因子、微调按这个顺序轮流补一批。
没有待领取的自动任务时立刻补下一种;距上一批已满 2 小时也会再补一批。
切片先不绑定节点,心跳到来时再改成 processing。高 IC 因子写入 alpha_factors。
"""
@@ -11,7 +12,7 @@ import secrets
import uuid
from datetime import datetime, timedelta, timezone
from sqlalchemy import func, select
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.clock import utcnow
@@ -22,6 +23,14 @@ ALPHA_BATCH_HOURS = 2
MIN_ABS_IC = 0.03
SLICE_COUNT = 3
QUEUE_HOLD_DAYS = 7
AUTO_CYCLE = ("llm_inference", "alpha_mining", "lora_training")
LLM_MODEL = "qwen2.5-3b-instruct-q4_k_m"
LORA_BASE = "qwen2.5-3b"
PROMPTS = (
"用三句话说明去中心化算力网络做什么",
"列出显卡推理时的三个资源限制",
"解释量化因子的 IC 表示什么",
)
OPERATORS = (
"ts_mean",
"ts_std",
@@ -35,41 +44,44 @@ OPERATORS = (
SYMBOLS = ("BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT")
def maybe_generate_auto_batch(
session: Session,
now: datetime | None = None,
force: bool = False,
) -> list[ComputeTask]:
"""没有待领取的自动任务,或距上一批已满 2 小时,就按轮询生成下一种任务。"""
moment = now or utcnow()
pending, latest = _auto_queue_state(session)
due = latest is None or moment - latest >= timedelta(hours=ALPHA_BATCH_HOURS)
if pending and not due and not force:
return []
kind = _next_cycle_type(session)
if kind == "llm_inference":
return _create_llm_batch(session, moment)
if kind == "lora_training":
return _create_lora_batch(session, moment)
return _create_alpha_batch(session, moment)
def maybe_generate_alpha_batch(
session: Session,
now: datetime | None = None,
force: bool = False,
) -> list[ComputeTask]:
"""队列里没有 pending 的挖掘任务,或距上一批已满 2 小时,就生成新的一批切片。"""
moment = now or utcnow()
pending = session.scalar(
select(func.count())
.select_from(ComputeTask)
.where(
ComputeTask.task_type == "alpha_mining",
ComputeTask.status == TaskStatus.PENDING,
)
) or 0
latest = _as_utc(
session.scalar(select(func.max(ComputeTask.dispatched_at)).where(ComputeTask.task_type == "alpha_mining"))
)
due = latest is None or moment - latest >= timedelta(hours=ALPHA_BATCH_HOURS)
if pending and not due and not force:
return []
return _create_alpha_batch(session, moment)
return maybe_generate_auto_batch(session, now=now, force=force)
def assign_queued_alpha_task(
def assign_queued_auto_task(
session: Session,
node: ComputeNode,
now: datetime | None = None,
) -> ComputeTask | None:
"""把最早的一条未绑定切片派给当前节点。节点已有执行中的挖掘任务时不再加派。"""
"""把最早的一条未绑定自动任务派给当前节点。节点已有执行中的自动任务时不再加派。"""
moment = now or utcnow()
holding = session.scalar(
select(ComputeTask.id).where(
ComputeTask.node_id == node.id,
ComputeTask.task_type == "alpha_mining",
ComputeTask.task_type.in_(AUTO_CYCLE),
ComputeTask.status == TaskStatus.PROCESSING,
)
)
@@ -78,7 +90,7 @@ def assign_queued_alpha_task(
task = session.scalar(
select(ComputeTask)
.where(
ComputeTask.task_type == "alpha_mining",
ComputeTask.task_type.in_(AUTO_CYCLE),
ComputeTask.status == TaskStatus.PENDING,
ComputeTask.node_id.is_(None),
)
@@ -94,6 +106,14 @@ def assign_queued_alpha_task(
return task
def assign_queued_alpha_task(
session: Session,
node: ComputeNode,
now: datetime | None = None,
) -> ComputeTask | None:
return assign_queued_auto_task(session, node, now=now)
def ingest_alpha_factors(session: Session, task: ComputeTask) -> list[AlphaFactor]:
"""成功回传的挖掘结果里,只保存公式非空且绝对 IC 达到门槛的因子。"""
if task.task_type != "alpha_mining" or task.status != TaskStatus.COMPLETED or not task.success:
@@ -125,37 +145,140 @@ def ingest_alpha_factors(session: Session, task: ComputeTask) -> list[AlphaFacto
return saved
def _create_llm_batch(session: Session, moment: datetime) -> list[ComputeTask]:
seed, rng, batch_id = _batch_identity()
prompts = rng.sample(PROMPTS, k=SLICE_COUNT)
tasks = [
_queue_slice(
session,
moment,
"llm_inference",
{
"auto": True,
"batch_id": batch_id,
"seed": seed,
"model": LLM_MODEL,
"prompt": prompt,
"max_tokens": 256,
"nonce": secrets.token_hex(16),
},
)
for prompt in prompts
]
session.flush()
return tasks
def _create_lora_batch(session: Session, moment: datetime) -> list[ComputeTask]:
seed, _rng, batch_id = _batch_identity()
tasks = [
_queue_slice(
session,
moment,
"lora_training",
{
"auto": True,
"batch_id": batch_id,
"seed": seed,
"base_model": LORA_BASE,
"rank": rank,
"steps": 100,
"nonce": secrets.token_hex(16),
},
)
for rank in (8, 16, 32)
]
session.flush()
return tasks
def _create_alpha_batch(session: Session, moment: datetime) -> list[ComputeTask]:
seed = secrets.randbelow(2**31)
rng = random.Random(seed)
batch_id = uuid.uuid4().hex[:12]
seed, rng, batch_id = _batch_identity()
combos = _operator_combos(rng, SLICE_COUNT)
symbols = rng.sample(SYMBOLS, k=SLICE_COUNT)
deadline = moment + timedelta(days=QUEUE_HOLD_DAYS)
tasks: list[ComputeTask] = []
for combo, symbol in zip(combos, symbols):
task = ComputeTask(
id=str(uuid.uuid4()),
node_id=None,
task_type="alpha_mining",
payload={
tasks = [
_queue_slice(
session,
moment,
"alpha_mining",
{
"auto": True,
"batch_id": batch_id,
"seed": seed,
"operators": combo,
"symbol": symbol,
"nonce": secrets.token_hex(16),
},
status=TaskStatus.PENDING,
push_status=PushStatus.QUEUED,
dispatched_at=moment,
deadline_at=deadline,
)
session.add(task)
tasks.append(task)
for combo, symbol in zip(combos, symbols)
]
session.flush()
return tasks
def _queue_slice(session: Session, moment: datetime, task_type: str, payload: dict) -> ComputeTask:
task = ComputeTask(
id=str(uuid.uuid4()),
node_id=None,
task_type=task_type,
payload=payload,
status=TaskStatus.PENDING,
push_status=PushStatus.QUEUED,
dispatched_at=moment,
deadline_at=moment + timedelta(days=QUEUE_HOLD_DAYS),
)
session.add(task)
return task
def _batch_identity() -> tuple[int, random.Random, str]:
seed = secrets.randbelow(2**31)
return seed, random.Random(seed), uuid.uuid4().hex[:12]
def _auto_queue_state(session: Session) -> tuple[int, datetime | None]:
rows = session.scalars(
select(ComputeTask).where(
ComputeTask.task_type.in_(AUTO_CYCLE),
ComputeTask.node_id.is_(None),
ComputeTask.status == TaskStatus.PENDING,
)
).all()
pending = sum(1 for row in rows if (row.payload or {}).get("auto"))
latest = _latest_auto_dispatched(session)
return pending, latest
def _latest_auto_dispatched(session: Session) -> datetime | None:
rows = session.scalars(
select(ComputeTask)
.where(ComputeTask.task_type.in_(AUTO_CYCLE))
.order_by(ComputeTask.dispatched_at.desc(), ComputeTask.id.desc())
.limit(100)
).all()
for row in rows:
if (row.payload or {}).get("auto"):
return _as_utc(row.dispatched_at)
return None
def _next_cycle_type(session: Session) -> str:
rows = session.scalars(
select(ComputeTask)
.where(ComputeTask.task_type.in_(AUTO_CYCLE))
.order_by(ComputeTask.dispatched_at.desc(), ComputeTask.id.desc())
.limit(100)
).all()
latest = None
for row in rows:
if (row.payload or {}).get("auto"):
latest = row.task_type
break
if latest not in AUTO_CYCLE:
return AUTO_CYCLE[0]
return AUTO_CYCLE[(AUTO_CYCLE.index(latest) + 1) % len(AUTO_CYCLE)]
def _operator_combos(rng: random.Random, count: int) -> list[list[str]]:
combos: list[list[str]] = []
seen: set[tuple[str, ...]] = set()