LLM 101 Introducción

Vamos a hacer una serie de preguntas rápidas para ayudar a los alumnos a empezar a trabajar con modelos grandes del lenguaje o LLM (Large Language Model)

¿Que es un modelo grande del lenguaje o LLM del inglés Large Language Model?

Es una red neuronal profunda (con muchas capas) diseñadas para procesar lenguaje natural (NLP del inglés natural language processing). Esto es, entender y generar lenguaje natural escrito. Como todas las redes neuronales, hay que entrenarlas. Una LLM se entrena con mucho texto y «aprende» patrones complejos del lenguaje.

¿Por qué se han puesto de moda?

Hace unos años, unas arquitecturas llamadas Transformer y sobre todo, el entrenamiento masivo con toda la información que hay disponible en Internet ha hecho que los LLM tengan unas prestaciones increíbles en tareas concretas, mas allá de las que originalmente fueron diseñadas. chatGPT (Generative Pre-trained Transformer) de OpenAI y BERT (Bidrectional Encoder Representations form Transformer) de google son las arquitecturas mas famosas. Una descripción de la arquitectura transformer puedes verla aquí

¿Pero un LLM es un programa que ejecuto en mi ordenador?

No exactamente, es un archivo binario que contiene la configuración de la red neuronal después del entrenamiento, esto es, los «pesos» de cada neurona, de cada capa, calculados en el proceso de aprendizaje.
Los modelos pueden tener distintos formatos, generalmente indicados en el nombre. Aunque no hay un estándar de nombrado, se detectan patrones en el nombrado de los modelos. Por ejemplo, SynthIA-7B-v2.0-16k-AWQ es un modelo entrenado con 7B, siete billones de parámetros, en su versión 2.0 y con un tamaño de contexto de 16K. La AWQ indica el formato y la precisión en el cual se han almacenado los «pesos» de cada neurona.

¿Qué es el tamaño del contexto?

Una cosa es el modelo LLM y otra cosa son sus parámetros. El tamaño del contexto es el número máximo de tokens (palabras) que le puedes pasar al LLM. Lo define la arquitectura del LLM y generalmente, a mayor tamaño del contexto, mejor se puede especificar la tarea que le pides al LLM por lo que generará cosas mas precisas. También es cierto que generalmente, implica mas memoria. Es el prompt que le metes a chatGPT para que te responda.

Vale, pero ¿Cómo ejecuto un LLM en mi máquina?

Bien, tienes que cargar el modelo binario en memoria para poder introducirles contexto (el famoso prompt) y obtener lo que genera. Los modelos pueden ser muy grandes por lo que los requisitos hardware son exigentes. Dicho esto, hay librerías en varios lenguajes de programación que te permiten cargar en memoria e interactuar con LLMs mediante un sencillo programa.
Uno de los mejores recursos para trabajar en este campo y en IA en general es la web https:/huggingface.co , que tiene librerías, modelos, datasets, papers, etc. la guía de uso de esta página es un buen primer comienzo.

¿Me puedes poner un ejemplo?

El código python de mas abajo carga un modelo un modelo LLM de código abierto modesto, que se baja de la web https://huggingface.co/ e interacciona con él introduciéndoles prompts (el contexto que hablábamos antes) y obteniendo las respuestas. Realmente trata de predecir la siguiente palabra al prompt.

import time
import torch
import transformers
import accelerate

from transformers import AutoTokenizer, AutoModelForCausalLM

print("Loading model...")
print("transformer version: ", transformers.__version__)
print("accelerate version: ", accelerate.__version__)
timeStart = time.time()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device:", device)

hugginfacemodelname = "openchat/openchat-3.5-1210"
tokenizer = AutoTokenizer.from_pretrained(hugginfacemodelname)
model = AutoModelForCausalLM.from_pretrained(
    hugginfacemodelname,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
).to(device)

print("Load model time: ", -timeStart + time.time())

while True:
    input_str = input("Enter: ")
    input_token_length = input("Enter length: ")
    if input_str == "exit":
        break
    timeStart = time.time()
    inputs = tokenizer.encode(input_str, return_tensors="pt").to(device)
    outputs = model.generate(
        inputs,
        max_new_tokens=int(input_token_length),
    )
    output_str = tokenizer.decode(outputs[0])
    print(output_str)
    print("Time taken: ", -timeStart + time.time())

El código utiliza la librería python transformers y torch. En primer lugar se indica el modelo LLM a cargar, openchat/openchat-3.5-1210 y se genera el tokenizer que no es sino una herramienta que convierte el texto en tokens (palabras). Para el modelo que hemos especificado, la función «from_pretained» busca y carga el tokenizador correspondiente. Si se lo tiene que bajar, esto puede tardar. A continuación, con AutoModelForCausalLM.from_pretained.., se carga el modelo de lenguaje causal, un tipo de modelo que predice la siguiente palabra en una secuencia.

¿Hay mas tipos de modelos?

Si, en esa librería hay modelos específicos para pregunta-respuesta, para clasificación de imágenes, multimodales, etc. al cual le tienes que indicar qué modelo quieres entrenar.

¿El prompt o la entrada del LLM cómo se articula?

Asociado al concepto de LLM aparece el concepto de prompt engineering o ingeniería de prompts. Es importante destacar que un buen prompt o cadena de prompts puede mejorar mucho la salida de una determinada LLM en una tarea específica. En los últimos años aparece una gran cantidad de métodos para elaborar prompts:
Referencias: https://dl.acm.org/doi/pdf/10.1145/3560815

¿ Puedo entrenar un LLM en lo que yo quiera?

En principio si. Entrenar un LLM desde cero requiere de muchos recursos. La mayoría de los ingenieros hacen un proceso denominado finetunning que no es otra cosa que coger un modelo LLM ya entrado de forma genérica y aplicarles un proceso de entrenamiento con un conjunto de datos específico de la tarea donde quiero usar mi LLM. El resultado es un modelo LLM que tiene el conocimiento general y se ha especializado mediante lo que ha aprendido con los patrones del dataset con el cual has hecho el finetunning. Un ejemplo muy bueno que aparece en los vídeos de formación de google relacionados con LLM, es ver el finetunning similar al proceso de enseñarle a una mascota, por ejemplo un perro, una serie de tareas específicas (buscar droga, personas, órdenes, etc.). Tienes el perro, siendo en este simil, la LLM generalista, y mediante un proceso de entrenamiento (finetunning), obtienes una LLM que es mejor que la generalista en un proceso específico.

Otra técnica muy adecuada es el Retrieval Augmented Generation o RAG, que no es sino mejorar las respuestas de LLM en un dominio específico proporcionándole información específica de ese dominio. Lo que se hace es recuperar información relevante (de fuentes internas o externas) para la pregunta e introducirla en el contexto/prompt junto con la pregunta para obtener mejores respuestas. Realmente todo este proceso de recuperación lo que hace es mejorar el prompt.

Finetunning

En este enlace podemos ver los principios básicos de finetunning:
hay tres aproximaciones al finetuning

  1. Self-supervised learning
  2. Supervised learning
  3. Reinforcement learning

¿Por qué ocupa tanto una LLM?

Una red neuronal almacena pesos calculados en el proceso de entrenamiento, como toda
variable, esos pesos pueden almacenarse en diverso formatos (punto flotante vs entero) y con determinada precisión (64,32,16 bits). La precisión es importante por que es lo que almacena los patrones aprendidos.

¿Qué puedo hacer para reducir el tamaño de una LLM?

Hay un proceso, denominado cuantización, que pasa de una determinada precisión a otra. Si pasas de una precisión en punto flotante de 16 bits a un entero de 8 bits, esto hace que el modelo ocupe menos en el disco duro a costa de perder información/patrones. Es decir, el modelo es algo peor. Hay dos tipos de cuantización, simétrica y asimétrica Mas info: https://myscale.com/blog/es/quantization-for-llms-finetuning/

En el siguiente ejemplo podemos ver un ejemplo básico de finetunning para evaluar sentimiento en comentarios de películas. El ejemplo es muy sencillo pero completo, en primer lugar se evalúa con un pequeño testbench la LLM escogida, una de las mas sencillas, se entrena con un dataset de comentarios etiquetados y se vuelve a evaluar para ver si ha «aprendido». https://towardsdatascience.com/fine-tuning-large-language-models-llms-23473d763b91

¿Cómo evaluamos un LLM?

Cómo sabemos si una LLM es buena o malo y qué podemos probar para ver si hemos mejorado. Bueno, aquí entra en juego la evaluación de las redes neuronales en general. Es decir, evaluar las prestaciones en base a una tarea específica pasa inherentemente por poder evaluar cómo de bueno son los resultados obtenidos ante prompts especificos.

Dos paradigmas populares, para la evaluación genérica de LLMs:

  1. El tradicional NLP Benchmark approach usando, por ejemplo, MosaicML’s Eval Gauntlet.
  2. Otra técnica muy utilizada es , precisamente, utilizar una LLM como juez.

¿Qué LLM es mejor?

Con la aparición de múltiples LLMs han aparecido numerosas métricas y rankings en diferentes testbench. Aquí puedes ver el ranking de hugging face: link

Herramientas y recursos

https://pinokio.computer/ una WEBUI para instalar y lidiar con modelos de AI.
https://huggingface.co/TheBloke Distintos modelos y parámetros para poder ser ejecutados en máquinas mas modestas
https://github.com/jmorganca/ollama herramienta para ejecutar LLM de forma local y hacerle un wrapper python que te permita adaptar los prompts/salidas.

Terminología:

Cuantización: Reduce la precisión de los números utilizados para representar los pesos en el modelo, pierdes precisión pero el tamaño y la inferencia se acelera sin necesitar tantos recursos.
GPTQ : formato de cuantización
-AWQ : formato de cuantización
-GGUF : formato de modelos que reemplaza a GGML.
Tamaño del contexto: la cantidad de tokens (normalmente palabras ) que un modelo puede considerar en un prompt de una vez cuando genera una respuesta o predicción.
Formatos de los modelos models in open AI book
– Destilación (Distillation) es el proceso de entrenar un modelo pequeño (llamado estudiante) para emular las respuestas de un modelo mas grande (profesor).
-Pruning es el proceso de elminar las salidas no críticas del modelo pequeño o estudiante.
-Quantization es el proceso de reducir la precisión de los pesos del modelo y las activaciones.

Bibliografía

Open source LLM
XGen-7B Technical Report un LLM open source con un prompt de hasta 8K

Permisos de archivos en GNU/Linux

Los permisos de acceso a los archivos en GNU/Linux limitan quién puede acceder a un archivo y qué puede hacer con ese archivo. Se define el perfil de propietario del archivo (el usuario que lo creó), el grupo al cual pertenece dicho archivo (que por defecto es al grupo del usuario que lo creó), y el resto de usuarios que no son ni el propietario, ni pertenecen al grupo al cual pertenece el archivo. Para cada uno de esos perfiles se define si el usuario puede acceder para leer el archivo, escribir en él o ejecutarlo.
¿Cómo saber qué permisos tiene un archivo?, bien, la forma más fácil es con el comando ls, indicando que te dé todos archivos, incluso los que empiezan por «.» con la opción -a. En la figura podemos ver un ejemplo con la opción l para que liste los archivos, y la h, si quieres que el tamaño salga en múltiplos cómodos de leer (si no, sale un número en bytes). En la figura podemos ver cómo interpretar toda la salida del comando ls -alh en un directorio con un único archivo (hola.txt).

Atendiendo a la información que muestra el comando leída de izquierda a derecha, podemos ver que el archivo hola.txt es un archivo, el propietario tiene permisos de lectura y escritura, el grupo y el resto de usuarios solo pueden leerlo, tiene un único enlace a dicho archivo (el 1 sin leyenda), lo creó el usuario alumno, pertenece al grupo de alumno, tiene 5 bytes, se creó el 18 de enero a las 11:56 y se llama hola.txt.
¿Cómo puedo cambiar los permisos de un archivo?, con el comando chmod, por ejemplo, si pudiéramos ejecutar el archivo hola.txt y quisiéramos darle permisos de ejecución, bastaría con chmod +x hola.txt

Comandos, opciones y argumentos en la consola GNU/Linux

Vamos a repasar los conceptos de comando, opciones y argumentos mientras continuamos gestionando archivos. Si tienes claro estos conceptos avanzarás más rápido en tu dominio de la consola.
¿Qué es un comando? es una orden que se ejecuta en el terminal y que ejecuta un programa con una funcionalidad determinada. Por ejemplo, la orden ls ejecuta el programa ls que lista por pantalla los archivos de un directorio.
¿Qué es una opción? es una indicación, que se pone a continuación del comando, para el programa que se ejecuta para que haga o muestre algo diferente. Generalmente, en su forma corta es una letra precedida por un guion, por ejemplo -l. En el comando ls, la opción -l lista los archivos en forma de lista con sus permisos, usuario, grupo, tamaño, fecha de creación y nombre. De esta forma, si queremos visualizar los archivos de esta forma, debemos añadir la opción -l a la orden ls: ls -l. Algunas opciones pueden tener un formato largo que es una palabra, precedida por dos guiones, ej. – -help. La opción – -help en la mayoría de los comandos imprime una ayuda básica con las opciones disponibles.
¿Qué es un argumento? es de nuevo una indicación, que generalmente se pone a continuación del comando y las opciones, sobre qué directorio o archivo hay que ejecutar el programa indicado. Por ejemplo, si queremos listar los archivos de un directorio concreto, lo tenemos que indicar al final de la orden. ls -l /home/usuario/directorio listará en forma de lista, los archivos del directorio /home/usuario/directorio.

Si no indicas ninguna opción y/o argumento, el programa tiene habilitadas unas opciones por defecto que no necesitan indicarse. El comando ls por defecto lista los archivos del directorio donde se ejecuta.
La estructura de archivos y directorios tiene forma de árbol donde el «tronco» sería el elemento raiz o directorio inicial / y luego está formado por subdirectorios del sistema operativo y, dentro del directorio «home», cada usuario tiene un directorio para sus archivos personales.
En la figura de arriba fíjate que hay dos formas equivalentes de indicar que liste los subdirectorios y archivos del directorio uclm.
El comando man es el comando de ayuda del terminal y si le proporcionas como argumento el comando que sea, te mostrará la ayuda de ese comando (si tiene ayuda).

Comandos básicos de la consola, gestión de archivos (Debian 10: Bash)

Entre los comandos más habituales que ejecutamos en la consola nos encontramos todo lo que tiene que ver con la gestión de archivos y directorios. El directorio inicial o raiz viene representada por la barra / y es equivalente al directorio C:\ de windows. A partir de esa ruta inicial se crea una estructura de directorios y archivos con sus características asociadas.

Con el comando ls vemos los archivos y directorios del directorio que le indiquemos. Si no ponemos nada, por defecto, lista los directorios y archivos del directorio donde se ejecuta.

Se utiliza la barra / para separar cada directorio, de esta forma, /home/alumno/ejemplo/ es un directorio home que se encuentra en el directorio raíz y dentro del cual hay un directorio llamado alumno que a su vez alberga un directorio ejemplo. En bash, el punto «.» representa el directorio actual donde se encuentra la consola, los dos puntos «..» representa el directorio anterior en la ruta al cual está la consola, y el símbolo de la ñ «~» representa el directorio de usuario actual, por ejemplo, si el usuario es alumno, la ~ representaría la ruta /home/alumnno
Con el comando «cd DIRECTORIO» nos vamos al directorio que le indiquemos a continuación como argumento.
De esta forma, si nos encontramos en el directorio /home/alumno, y queremos ir al directorio ejemplo que se encuentra en dicho directorio, podemos especificar la ruta de varias formas:

  1. La ruta relativa al directorio donde se ejecuta el comando: cd ejemplo
  2. La ruta absoluta: cd /home/alumno/ejemplo
  3. La ruta relativa al directorio actual indicado explícitamente: cd ./ejemplo
  4. La ruta relativa al directorio padre del actual: cd ../alumno/ejemplo

Con el comando «mkdir DIRECTORIO» creamos un directorio o directorios si no existen. Por defecto lo creamos en el directorio actual pero se pueden especificar rutas completas. Creemos el directorio ejemplo2, pondremos varias formas para afianzar el concepto de ruta:

  1. mkdir ejemplo2
  2. mkdir /home/alumno/ejemplo2
  3. mkdir ~/ejemplo2

En la figura de abajo podemos ver un ejemplo creando un directorio uclm con la facultad esi dentro de ella y el edificio fermin-caballero, a su vez, dentro de la esi. Finalmente, con el comando cd, nos vamos al último directorio creado.

Comandos de la consola GNU/Linux (Debian 10: Bash)

Un comando es una orden que interpreta el intérprete de comandos y que sirve para interaccionar con el sistema operativo. Hay órdenes para realizar todo, gestionar archivos, ejecutar aplicaciones, administrar servicios de red, explorar internet, diagnosticar tu computador, etc.
Un comando está formado por el nombre del comando, una o varias opciones que modifican el comportamiento del comando y uno o varios argumentos que indican la ruta o archivo con el que el comando va a trabajar. Las opciones tienen un formato corto, una letra precedida por un guión y un formato largo, una palabra que indica la opción precedida por dos guiones. Aunque depende del programador que realizó el comando, algo más o menos estándar es que la opción -h imprima la ayuda del comando. Esa misma opción tiene un formato largo –help. Como ya he comentado, aunque existen prácticas comunes en cuanto a las opciones asociadas a la palabra en inglés que identifica la opción (h para la ayuda, v para sacar información de los pasos que está haciendo, etc.), cada comando puede seguir sus propias reglas.

Antes de empezar a ver ejemplos tres cosas que debes tener en cuenta:

  1. Los intérpretes de comandos en linux son sensibles a las mayúsculas por lo tanto ls es un comando distinto a LS. Por lo general, los nombres de comandos todos en minúsculas. Lo mismo para las opciones, generalmente, no es lo mismo -p que -P, aunque esto depende del creador del comando y, aunque recomendable, no está tan estandarizado.
  2. El tabulador autocompleta nombres de comandos, de directorios y de archivos. Aquí reside gran parte de la potencia de la consola y de su alta productividad, acostúmbrate a utilizar el tabulador. Generalmente la configuración por defecto está habilitada, si no, hay que habilitarla. Si quieres usar el comando mkdir, teclea mk y pulsa tabulador, si hay más de un comando que empieza por mk no hará nada, pulsa otra vez el tabulador y te sugerirá todos los comandos que empiezan por mk, sigue tecleando y cuando no sea ambiguo, si pulsas tabulador te lo completará.
  3. No hace falta «estudiarte» los comandos, poco a poco, los que más uses se te irán quedando, es buena idea imprimirte una hoja de comandos (por ejemplo, esta, esta otra, o esta) al principio para ir mirando los más habituales. En breve no te hará falta

Vamos a ver un ejemplo antes de seguir. El comando ls lista los directorios y archivos que hay dentro de un determinado directorio. En el gif de abajo, la primera vez que ejecutamos el comando ls, no ponemos ninguna opción ni argumento. Esto hace que ls coja las opciones y argumentos habilitadas por defecto, con lo cual, nos saca un listado de nombres con un código de colores (los azules son directorios) y del directorio donde se ejecuta ls.

Si queremos mas información podemos usar la opción -l, que te lista los archivos y directorios con mucha más información (que veremos en una futura entrada) del directorio en el cual ejecutamos el comando.
Si queremos listar los archivos y directorios, debemos indicárselo al comando ls, eso es lo que hacemos en las dos últimas ejecuciones, indicándole que liste el directorio padre de donde estoy actualmente (se indica con los dos puntos ..) por lo que lista el directorio del usuario alumno, y que liste el directorio ejemplo (que solo tiene una carpeta llamada uclm).

En ambos casos le agrego la opción -l para que liste los detalles de cada directorio o archivo que encuentre.

Pero ¿cómo puedo saber qué opciones y argumentos acepta un comando?, bueno, hay generalmente dos formas de ver qué opciones y argumentos contempla un comando (aparte de buscarlo en google) sin salir de dentro del terminal.

  1. Las opciones de ayuda del propio comando (los mas habituales –help o -h)
  2. Usar el comando de ayuda en linea man. El comando man toma como argumento de entrada cualquier comando y te muestra la ayuda si está disponible en el computador

Simulando consumo de energía en NS3

El consumo de energía es un parámetro importante sobre todo en escenarios de nodos alimentados por batería. Tener el perfil de consumo de un nodo no es tarea fácil y menos estimar su tiempo de vida conforme a unos parámetros de transmisión, encendido, apagado, etc.
Por ello hacer simulaciones de energía puede ser una buena práctica para muchos proyectos.

En NS3 se modela el consumo de energía haciendo una abstracción de los elementos que existen en la realidad. En este enlace puedes consultar la documentación oficial.

Los tres elementos principales son, las fuentes de energía, el modelo de energía de un dispositivo y un recolector de energía.

Una fuente de energía (Energy Source) es, como su propio nombre indica, una batería o elemento proveedor de energía que se conecta a un dispositivo para suministrarle energía de acuerdo a un modelo de energía de ese dispositivo.
Un modelo de energía de un dispositivo es un modelo de consumo de acuerdo a una serie de estados en los cuales un dispositivo puede estar. Por ejemplo, un dispositivo inalámbrico puede estar dormido, enviando o recibiendo información y tendrá un consumo distinto en función de cada uno de esos tres estados estado. Por lo tanto, debemos conectar ese modelo de energía con los estados del dispositivo.

Finalmente el recolector de energía sirve para modelar un dispositivo (e.g un panel solar) y el entorno (e.g. radiación solar) para proveer energía a una fuente de energía de cara a su recarga.

Cada uno de esos tres elementos se configuran mediante su correspondiente asistente (Energy source helper, Device Energy Model Helper y Energy Harvesting Helper) que te ayuda a configurar los parámetros básicos mediante atributos.

Los asistentes de modelos concretos heredan de esos asistentes con las particularidades de cada modelo/tecnología. Por ejemplo, hay un asistente para configurar una batería que hereda del Energy source helper y que añade los elementos específicos que caracterizan a una batería de ese tipo partiendo de los parámetros básicos del modelo:

  • Energía inicial(J)
  • Voltage de partida (V)
  • Tiempo de actualización del nivel de la batería

Por ejemplo, una batería del tipo RV añade parámetros como los valores de voltage de corte, valores alfa, beta, etc.
Existen modelos de batería de Litio Y rv.

De igual forma, el modelo de consumo de energía de un dispositivo se realiza asociándolo a una tecnología concreta, modelando cada uno de los estados y su consumo en Amperios. El modelo mas estudiado y modelado es el modelo wifi que define el consumo para hasta 7 estados distintos.
Para los dispositivos con varias interfaces (e.g. pasarelas con varias interfaces inalámbricas), una fuente de energía puede estar conectado a varios modelos de consumo.

Veamos el ejemplo básico que viene con la instalación de ns3, ejecutamos el ejemplo energy-model-example y vemos la salida que nos proporciona:

ns-allinone-3.31/ns-3.31$ ./waf --run energy-model-example
Waf: Entering directory `/home/felix/tools/ns-allinone-3.31/ns-3.31/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.31/ns-3.31/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (2.409s)
+0.000000000s -1 Assign IP Addresses.
0.000574667s Current remaining energy = 0.0995293J
0.000574667s Total energy consumed by radio = 0.000470652J
0.000762667s Current remaining energy = 0.0993754J
0.000762667s Total energy consumed by radio = 0.000624624J
0.00287467s Current remaining energy = 0.0973922J
0.00287467s Total energy consumed by radio = 0.00260779J
--
Received one packet! Socket: 10.1.1.1 port: 49153 at time = 0.00287467
--
0.106594s Total energy consumed by radio = 0.0875537J
0.106594s Current remaining energy = 0.0124463J
End of simulation (10s) Total energy consumed by radio = 0.0982333J
End of simulation (10s) Total energy consumed by radio = 0.0875537J

en el archivo $/ns-3.31/examples/energy/energy-model-example.cc podemos ver ese ejemplo. Es un ejemplo muy básico wifi donde se configura, mediante asistentes, una fuente de energía y se asocia a un modelo de consumo de wifi. Un aspecto a tener en cuenta es que si instalas el módulo de lorawan, también existe un ejemplo de energía energy-model-example.cc por lo que waf puede ejecutar el ejemplo que se encuentre antes en el orden de búsqueda. Si quieres asegurarte ejecutar el ejemplo citado, pon la ruta completa ./waf --run examples/energy/energy-model-example

Bien, expliquemos el ejemplo wifi, lo primero, creamos un asistente de fuentes de energía y lo configuramos con una energía inicial de 0.1 Julios.

BasicEnergySourceHelper basicSourceHelper;
basicSourceHelper.Set ("BasicEnergySourceInitialEnergyJ", DoubleValue (0.1));

Instalamos esa fuente de energía en todos los nodos de un contenedor previamente configurado (NodeContainer c) con los nodos wifi (2).

EnergySourceContainer sources = basicSourceHelper.Install (c);

A continuación configuramos el modelo de consumo que queremos para dichos dispositivos:

WifiRadioEnergyModelHelper radioEnergyHelper;
radioEnergyHelper.Set ("TxCurrentA", DoubleValue (0.0174));

Vemos que sólo configuramos el consumo cuando se transmite dejando el resto de consumos por defecto.
Finalmente, con las fuentes de energía y los modelos, creamos un contenedor asociando fuentes de energía y modelos de consumo:

DeviceEnergyModelContainer deviceModels = radioEnergyHelper.Install (devices, sources);

El resto de la configuración es exactamente igual a cualquier otro ejemplo de ns3 relacionado con WiFi.
Un aspecto a destacar es cómo se imprime la información de la energía restante en la batería. El mecanismo es algo diferente a lo visto en este tutorial y lo describiremos en detalle en otra entrada. Básicamente se asocia una función al cambio en el estado de la energía remanente de forma que cuando cambia esa variable se llama a esa función que imprime el valor.

Este mecanismo lo describiremos en profundidad en otra entrada.

El sistema de trazas de NS3

Una vez que hemos creado una simulación necesitamos analizar los resultados de dicha simulación. Cómo sacar los resultados de una simulación es responsabilidad del sistema de trazas de NS3. El sistema de trazas de NS3 genera, como veremos a continuación, una serie de archivos en un formato determinado que nos muestra todos los resultados de nuestra simulación. Estos formatos están definidos en los módulos existentes en NS3 y podemos añadirlos a nuestros nuevos módulos. En esta entrada vamos a centrarnos en sacar información de los módulos existentes mediante un formato determinado (pcap).

Existen dos formatos que podemos generar, archivos con extensión .tr que es heredado del simulador NS2 y archivos con extensión .pcap que es un estándar para guardar paquetes de red. Por supuesto, al ser un archivo en C++ podemos generar resultados en otros formatos, pero supondría acceder a la información directamente lo cual es más complejo e ineficiente.

No obstante, personalmente recomiendo el uso de pcap, ya que es un estándar abierto soportado por multitud de herramientas que podemos utilizar para analizar los resultados de nuestra simulación. Por ejemplo tenemos wireshark si queremos analizar el diálogo de un protocolo en nuestra simulación o scapy si queremos generar gráficas específicas mediante scripts python.

Los módulos mas relevantes de NS3 ya tienen implementados un asistente para generar trazas. Estos asistentes o Helpers nos permiten generar de forma fácil las trazas más relevantes. Vamos a generar trazas de nuestro ejemplo básico para ver cómo funciona. Tomamos nuestro basic-iot-sensors.cc y vamos a generar un archivo pcap por cada interfaz creada de nuestros tres nodos (Recordar que un nodo puede tener varias interfaces).

En principio, con habilitar la traza pcap en la capa física de wifi nos vale con lo que nos basta añadir:

wifiPhy.EnablePcapAll("resultados");

Si compilamos nuestro ejemplo de nuevo veremos que se han generado archivos pcap por cada interfaz con el formato resultados-NodeId-DeviceId.pcap. En nuestro caso tendríamos tres nodos con una única interfaz por lo que se generan los archivos resultados-0-0.pcap, resultados-1-0.pcap y resultados-2-0.pcap.
Captura wireshark

Todas las clases que heredan de la clase PcapHelperForDevice tiene este mecanismo para habilitar el pcap en todas las interfaces (Devices). Estas clases son (imagen extraída de la documentación de la clase PcapHelperForDevice):
Captura wireshark

Si se está interesado en cambios de estado de variables concretas, definidas como traceables dentro del modulo. Esto es, se pueden monitorizar sus cambios de estado, podemos definir funciones que se invoquen cuando hay un cambio en el valor de una variable. En la entrada relativa a observar el consumo de energía vemos un ejemplo de cómo observar este tipo información relativa a variables concretas (e.g. nivel de carga de una batería). Identificarás en las clases qué variables pueden ser traceadas por que son declaradas mediante una plantilla TracedValue

El sistema de log de NS3

Un buen entorno de logging te permite depurar y entender qué está pasando en tu simulación, así como entender cómo está estructurado el simulador NS3 y sus diferentes módulos.
El sistema de logging de NS3 se usa mediante variables de entorno y mediante el propio código de tu simulación si quieres habilitar logging en tu propia simulación.

Se establecen siete niveles de log proporcionando de menos a mas información:

  1. NS_LOG_ERROR: mensajes de error
  2. NS_LOG_WARN: mensajes de aviso
  3. NS_LOG_DEBUG: mensajes específicos de depuración
  4. NS_LOG_INFO: mensajes de información genéricos
  5. NS_LOG_FUNCTION: mensajes de llamadas a funciones para la trazabilidad de llamadas
  6. NS_LOG_LOGIC: mensajes de log con el flujo lógico dentro de cada función
  7. NS_LOGIC_ALL: todos los mensajes

Adicionalmente hay un nivel incondicional que imprime la salida con independencia de los niveles de log activos o no. Este es NS_LOG_UNCOND
¿Cómo visualizamos la información de log en ns3?, bien, vamos a comenzar con nuestro ejemplo IoT básico donde, precisamente, uno de los problemas es que no generábamos ningún tipo de información acerca de la simulación.
Efectivamente si lo ejecutamos, habiendo colocado previamente el archivo basic-iot-sensors.cc en el directorio scratch:

$./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (2.047s)

Vemos que no sale ningún tipo de información útil acerca de la simulación salvo que se ha compilado y ejecutado sin errores.
Vamos a estudiar la información que nos genera el módulo de YansWifiPhy, para ello:

$ export NS_LOG=YansWifiPhy
felix@homer:~/tools/ns-allinone-3.30.1/ns-3.30.1$ ./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `//ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.701s)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x5641614960f0)
+0.000000000s -1 YansWifiPhy:SetChannel(0x5641614960f0, 0x564161490400)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x56416153d640)
+0.000000000s -1 YansWifiPhy:SetChannel(0x56416153d640, 0x564161490400)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x564161541e90)
...

vemos que usamos la variable de entorno NS_LOG para fijar el módulo en el que estamos interesados. Si ponemos NS_LOG a un nombre de un módulo que no existe se imprimirá un listado de todos los módulos que NS3 tiene implementados. El nivel de log por defecto en la mayoría de los módulos será todos, podemos especificarlo también a la hora de definir el módulo. Por ejemplo vamos a definir el nivel info para el módulo UdpClient:

$ export NS_LOG=UdpClient=level_info
$ ./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.480s)
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 0 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 1 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 2 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 33 Time: 2.5
....

Vemos que ahora sale la información de las aplicaciones UDP instaladas en los nodos IoT enviando información. Substituye level_info por level_all en el primer comando para que veas como sale mucha más información.
Si algún módulo no se usa en tú simulación y pones NS_LOG a ese módulo no saldrá ninguna información adicional.
Si quieres indicar que el nombre de la función que genera el mensaje también se imprima, puedes hacer un OR con prefix_fund. En bash necesitarás comillas:

$export 'NS_LOG=UdpClient=level_all|prefix_func'

Para habilitar varios módulos, puedes encadenar usando : varios módulos.

$ export 'NS_LOG=UdpClient:UdpServer'

Vamos a crear un módulo para nuestra simulación y a sacar nuestro mensaje de log. Para ello, edita el archivo basic-iot-sensors.cc y añade estas dos líneas, por ejemplo, justo debajo de donde se inicia la función main:

int main (int argc, char *argv[]){
NS_LOG_COMPONENT_DEFINE ("IotEjemplo");
NS_LOG_INFO ("Creando la simulación");

Aunque ya estaba incluido en dicho archivo, si lo utilizas en tu simulación acuerdate incluir los archivos de cabezera de log («ns3/log.h»).
En la primera función definimos el módulo IotEjemplo y en la segunda sacamos nuestro primer mensaje. Si ahora queremos ver esa información:

$export 'NS_LOG=IotEjemplo'
$ ./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
[2876/2963] Compiling scratch/basic-iot-sensors.cc
[2921/2963] Linking build/scratch/basic-iot-sensors
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (6.339s)
IotEjemplo:main(): [INFO ] Creando la simulación

Donde, como podemos ver, se muestra nuestro mensaje.

Es buena idea usar el mecanismo de log en cualquier simulación de ns3 pero se hace imprescindible si aspiras a crear un nuevo módulo para el simulador destinado a ser usado por la comunidad.

Ejemplo básico NS3: IoT y sensores

Como ya hemos comentado en las primeras secciones, hay numerosos ejemplos y test que te sirven de ayuda y como punto de partida, vamos a realizar un ejemplo básico intentando simular una red de sensores que mandan información a una pasarela (Gateway) o nodo central. El objetivo de este ejemplo es ir completándolo con otras funciones muy necesarias en simulación (Log, trazas, configuración, varias tecnologías, energía, etc.) en entradas posteriores. En el repositorio del tutorial, el archivo de inicio es basic-iot-sensors.cc.
Lo primero que vamos a realizar es crear 3 nodos (variable numsensors) y posicionarlos en un punto determinado (aleatorio). Utilizamos una función que crea un escenario con un radio (variable radio) determinado en metros y, a continuación, le pasamos el contenedor de nuestros nodos para que posicione, de forma aleatoria, dichos nodos;

NodeContainer nodes;
nodes.Create(numsensors);
MobilityHelper scenario = createscenario(radio);
scenario.Install (nodes);

Como ya indicamos en la entrada de topología , se crea un escenario mediante el MobilityHelper con la configuración deseada:

MobilityHelper createscenario(double radio)
{
MobilityHelper mobility;
mobility.SetPositionAllocator ("ns3::UniformDiscPositionAllocator",
"rho", DoubleValue (radio),
"X", DoubleValue (0.0),
"Y", DoubleValue (0.0),
"Z", DoubleValue (0.0));

mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel");
return mobility;
}



Del contenedor de nodos, creamos un centro de la estrella donde posicionaremos el primer nodo, que de ahora en adelante representará la pasarela:

MobilityHelper gatewayposition = createstarcenter();
gatewayposition.Install (nodes.Get(0));

Una vez creados los dispositivos y emplazados (la pasarela en la posición x,y,z a 0,0,0 y el resto de forma aleatoria), vamos a seguir configurando nuestros nodos, configurando una interfaz wifi en cada uno de los nodos. La tecnología inalámbrica WIFI está de sobra probada en NS3 y nos sirve de punto de partida para nuestro ejemplo.
Para la creación de NetDevice por cada Nodo e instalarlo en cada uno de los nodos, usaremos los asistentes, nos crearemos un asistente para la capa física (wifiPhy), la capa Mac (wifiMac) y el objeto que modela el canal inalámbrico (wifiChannel) y al cual deberemos conectar todos los objetos que modelan la capa física.

WifiHelper wifi;
YansWifiPhyHelper wifiPhy = YansWifiPhyHelper::Default ();
YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default ();
WifiMacHelper wifiMac;
wifiMac.SetType ("ns3::AdhocWifiMac");
wifiPhy.SetChannel(wifiChannel.Create());
NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

En este caso para el modelado de la capa física y el canal utilizamos el modelo Yans. Es el modelo por defecto que se utiliza pero, obviamente, el El módulo wifi es uno de los más completos y estudiados con varios modelos disponibles.
El tipo de MAC es ns3::AdhocWifiMac que modela relaciones punto a punto entre dos dispositivos con Wifi. A continuación establecemos el canal en el asistente wifiPhy para que todos los objetos creados de la capa física compartan el canal y se puedan comunicar entre ellos. Estas configuraciones, en la parte wifi, son las configuraciones por defecto. Por último, instalamos usando el asistente general del wifi la capa física y la MAC en todos los nodos. El asistente de wifi nos devuelve un contenedor con los NetDevices creados (y asociados a los nodos):

NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

Hemos usado la configuración por defecto, este es uno de los puntos más importantes en cuanto a una simulación, tienes que definir claramente las métricas en las cuales estás interesados y modelar de forma simple el resto de elementos. Por ejemplo, si estás interesado en un protocolo de comunicaciones de la capa de aplicación y quieres validar su funcionalidad, la capa física/MAC que utilices debe ser modelada de la forma más simple posible.

Si por contra, estás interesado en modelar un nuevo algoritmo de control de flujo de 802.11ax, es la capa de aplicación la que debe modelar lo mínimo para generar el tráfico necesario para probar la funcionalidad.

Hasta aquí hemos creado el «Hardware» de nuestra simulación, vamos a instalar y configurar el resto de elementos.

Pasemos a la parte de configuración de la pila de protocolos, en este caso vamos a instalar y configurar una pila de protocolos TCP/IP, para ello creamos un asistente y lo instalamos en todos los nodos:

InternetStackHelper internet;
internet.Install (nodes);

Una vez instalada, configuramos la dirección de red y le asignamos direcciones IP a las interfaces de red instaladas en los nodos:

Ipv4AddressHelper ipv4;
ipv4.SetBase ("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer i = ipv4.Assign (devices);

en este caso, usamos la dirección de red 10.1.1.0 con máscara 255.255.255.0 y se les asigna, de forma consecutiva, a cada uno de los NetDevices en el container «devices» creando un contenedor de interfaces ya configuradas.

El último paso de creación de nuestro escenario propiamente dicho es la creación de aplicaciones que creen tráfico. Vamos a instalar un servidor UDP en la pasarela (nodo 0) y lo configuramos para que escuche en el puerto 2000 :

uint16_t port = 2000;
UdpServerHelper server(port);
ApplicationContainer gatewayapps = server.Install(nodes.Get(0));
gatewayapps.Start(Seconds(1.0));
gatewayapps.Stop(Seconds(11.0));

Como vemos en el código de arriba, creamos un asistente de servidor en el puerto deseado y lo instalamos en el nodo 0. Por último indicamos los momentos en que arrancamos y paramos el servidor en este caso sobre el contenedor que se ha creado. Un paso similar hay que hacer en los clientes, debemos crear clientes software configurados para enviar una cantidad de tráfico al servidor que acabamos de instalar en el todo 0 (y que obtenemos con nodes.Get(0)).
Obtenemos la dirección IP del servidor (la 0) del contenedor de interfaces, establecemos el tamaño en 32 bytes, que para la información generada por un sensor es suficiente. En principio generaremos como mucho 10 paquetes:

Address gatewayAddress = Address(i.GetAddress (0));
uint32_t packetSize = 32;
uint32_t maxPacketCount = 10;

Con esta configuración, se crea un asistente de cliente, se establece el intervalo de envío y se configura el cliente:

UdpClientHelper client (gatewayAddress, port);
Time interPacketInterval = Seconds (0.5);
client.SetAttribute("PacketSize", UintegerValue(packetSize));
client.SetAttribute ("Interval", TimeValue (interPacketInterval));
client.SetAttribute ("MaxPackets", UintegerValue(maxPacketCount));

Hay que resaltar cómo se configura, mediante el método SetAttribute, cada una de las características, indicando su nombre y valor, este mecanismo es muy utilizado por los objetos NS3 para la configuración de parámetros.
Por último, instalamos en los nodos un cliente (incluyendo la pasarela que actuará como sensor/servidor) creándose otro contenedor de aplicaciones que debemos arrancar y parar. Hay que asegurarse que el servidor esté arrancado antes que los clientes, por eso arrancamos en el segundo 2 y paramos mas tarde para no perder información.

ApplicationContainer apps = client.Install(nodes);
apps.Start(Seconds(2.0));
apps.Stop(Seconds(10.0));

Por último, arrancamos el simulador:

Simulator::Stop (Hours (24));
Simulator::Run ();
Simulator::Destroy ();

Los detalles del simulador lo veremos en otra entrada.

Con esto terminamos nuestra simulación, colocando el archivo en el directorio scratch lo ejecutamos:

/ns-3.30.1$ ./waf --run basic-iot-sensors
Waf: Entering directory `ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.747s)

Que compila y ejecuta nuestra simulación. Realmente no vemos ningún flujo de información por lo que no sabemos si todo ha ido bien, en próximas entradas veremos cómo obtener información, mediante log y trazas, de qué está pasando en nuestra simulación.

NS3: La clase NetDevice (Wifi)

La clase NetDevice representa una interfaz de red que va asociada a una tecnología. Es una clase abstracta de la cual heredamos para crear una interfaz de red de una tecnología asociada. Básicamente necesitamos tener en cuenta la capa MAC, la capa física y el canal.

Vamos a ver un ejemplo con la conocida tecnología WiFi. Cada módulo de tecnología de comunicación se estructura de forma distinta atendiendo al estándar, en el caso del módulo WiFi implementa muchas de las tecnologías del estándar IEEE 802.11 (802.11a, 802.11b, etc.) incluyendo los últimos estándares 802.11ac y 802.11ax así como algunas versiones especiales como 802.11p para redes vehiculares.

La clase YansWifiPhy implementa la capa física del estándar 802.11a en la cual tienes parámetros configurables como la frecuencia, la anchura del canal, el número del canal, configuraciones MIMO, etc. que son las propiedades de la clase.

Cabe destacar las variables TxGain y RxGain que expresan la ganancia en decibelios a la hora de transmitir y recibir respectivamente. Con estas variables podemos simular la antena de dicha interfaz.

Para conectar objetos de la clase YansWifiPhy necesitas un canal que simule cómo se comporta una transmisión WiFi con los parámetros que hayas configurado. La clase YansWifiChannel tiene este propósito. Realmente lo que hace este canal es configurar un ns3::PropagationLossModel y un ns3::PropagationDelayModel y aplicarlo a toda transmisión/recepción de un objeto YansWifiPhy. Como su propio nombre indica, esas clases modelan las perdidas y el retardo para la transmisión configurada. Es responsabilidad del programador configurar el canal con los modelos apropiados.

De la capa MAC definida en el estándar se encarga la clase WifiMac. Cuando creas un objeto de la clase WifiMac debes configurarle un objeto de la clase WifiPhy con el que interacciona. Puedes establecer con WifiMac parámetros para estándares específicos. Con funciones privadas, existen funciones para configurar estándares precisos, por ejemplo ns3::WifiMac::Configure80211a o Configure80211ax_2_4Ghz. En la lista de métodos privados de WifiMac tienes la lista completa.

Una vez tenemos el objeto de WifiMac y WifiPhy (por ejemplo YansWifiPhy) con su canal asociado, podemos construir el equivalente a la interfaz de rede Wifi con un NetDevice. Lo normal es utilizar un asistente ya que generalmente, tendremos que crear un NetDevice por cada nodo e instalarlo en dicho nodo. El asistente para este proceso es la clase WifiHelper. Una de las formas más comunes de usarlo es llamar al método install con un objeto WifiPhy, un objeto WifiMac y un NodeContainer. Este método creará un contenedor de objetos DeviceNet (NetDeviceContainer) asociados a cada nodo en el NodeContainer y con los correspondientes objetos instalados.

La clase que hereda de la clase NetDevice para wifi es la clase WifiNetDevice
Si echamos un vistazo a sus métodos, tenemos los métodos de configuración:

void SetMac (const Ptr< WifiMac > mac)
bool SetMtu (const uint16_t mtu)
void SetNode (const Ptr< Node > node)
void SetPhy (const Ptr< WifiPhy > phy)

Como hemos dicho anteriormente, la clase WifiHelper nos ayuda con esta configuración cuando tenemos que hacerlo con varios nodos.
Resumiendo, el esquema de recepción de un paquete con esta configuración sería, por lo tanto:
Channel -> WifiPhy -> WifiMac -> WifiNetDevice -> Node

En otra entrada analizaremos un ejemplo simplificado para ver cómo se configura un escenario simple. No obstante en el directorio examples de la instalación de NS3 hay un directorio wireless con ejemplos de Wifi y otras tecnologías inalámbricas.